diff --git a/example/enough_mail_example.dart b/example/enough_mail_example.dart index dfc56bed..9cbe24e8 100644 --- a/example/enough_mail_example.dart +++ b/example/enough_mail_example.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:enough_mail/enough_mail.dart'; String userName = 'user.name'; @@ -14,6 +15,7 @@ int smtpServerPort = 465; bool isSmtpServerSecure = true; void main() async { + //await mailExample(); await discoverExample(); await imapExample(); await smtpExample(); @@ -46,6 +48,42 @@ Future discoverExample() async { } } +Future mailExample() async { + var email = userName + '@domain.com'; + var config = await Discover.discover(email); + var incoming = MailServerConfig() + ..serverConfig = config.preferredIncomingServer + ..authentication = PlainAuthentication(userName, password); + var account = MailAccount() + ..email = email + ..incoming = incoming; + //TODO specify outgoing server configuration + var mailClient = MailClient(account, isLogEnabled: false); + await mailClient.connect(); + var mailboxesResponse = + await mailClient.listMailboxesAsTree(createIntermediate: false); + if (mailboxesResponse.isOkStatus) { + print(mailboxesResponse.result); + await mailClient.selectInbox(); + var fetchResponse = await mailClient.fetchMessages(count: 20); + if (fetchResponse.isOkStatus) { + for (var msg in fetchResponse.result) { + printMessage(msg); + } + } + } + mailClient.eventBus.on().listen((event) { + print('New message at ${DateTime.now()}:'); + printMessage(event.message); + }); + mailClient.startPolling(); + // print('flat:'); + // var mailboxesFlatResponse = await mailClient.listMailboxes(); + // if (mailboxesFlatResponse.isSuccess) { + // print(mailboxesFlatResponse.result); + // } +} + Future imapExample() async { var client = ImapClient(isLogEnabled: false); await client.connectToServer(imapServerHost, imapServerPort, diff --git a/lib/enough_mail.dart b/lib/enough_mail.dart index c8a2f64e..93b7761d 100644 --- a/lib/enough_mail.dart +++ b/lib/enough_mail.dart @@ -1,6 +1,6 @@ library enough_mail; -export 'imap/events.dart'; +export 'imap/imap_events.dart'; export 'imap/imap_client.dart'; export 'imap/mailbox.dart'; export 'imap/response.dart'; @@ -24,6 +24,13 @@ export 'codecs/quoted_printable_mail_codec.dart'; export 'discover/client_config.dart'; export 'discover/discover.dart'; +export 'mail/mail_account.dart'; +export 'mail/mail_authentication.dart'; +export 'mail/mail_client.dart'; +export 'mail/mail_events.dart'; +export 'mail/mail_response.dart'; +export 'mail/tree.dart'; + export 'mail_address.dart'; export 'media_type.dart'; export 'message_builder.dart'; diff --git a/lib/imap/imap_client.dart b/lib/imap/imap_client.dart index 6c7c258d..522aea8d 100644 --- a/lib/imap/imap_client.dart +++ b/lib/imap/imap_client.dart @@ -14,7 +14,7 @@ import 'package:enough_mail/src/imap/all_parsers.dart'; import 'package:enough_mail/src/imap/imap_response.dart'; import 'package:enough_mail/src/imap/imap_response_reader.dart'; -import 'events.dart'; +import 'imap_events.dart'; /// Describes a capability class Capability { @@ -85,7 +85,8 @@ class ImapClient { /// _log(event.eventType); /// }); /// ``` - EventBus eventBus; + EventBus get eventBus => _eventBus; + EventBus _eventBus; bool _isSocketClosingExpected = false; @@ -110,7 +111,8 @@ class ImapClient { /// /// Compare [eventBus] for more information. ImapClient({EventBus bus, bool isLogEnabled = false}) { - eventBus ??= EventBus(); + bus ??= EventBus(); + _eventBus = bus; _isLogEnabled = isLogEnabled ?? false; _imapResponseReader = ImapResponseReader(onServerResponse); } @@ -1201,7 +1203,7 @@ class ImapClient { _socket?.writeln('$id $command'); } - Future close() { + Future closeConnection() { _log('Closing socket for host ${serverInfo.host}'); _isSocketClosingExpected = true; return _socket?.close(); diff --git a/lib/imap/events.dart b/lib/imap/imap_events.dart similarity index 97% rename from lib/imap/events.dart rename to lib/imap/imap_events.dart index ee28a11c..44ba16fb 100644 --- a/lib/imap/events.dart +++ b/lib/imap/imap_events.dart @@ -21,7 +21,7 @@ class ImapExpungeEvent extends ImapEvent { /// Notifies about a sequence of messages that have been deleted. /// This event can only be triggered if the server is QRESYNC compliant and after the client has enabled QRESYNC. class ImapVanishedEvent extends ImapEvent { - /// Sequence of messages that have been expunged + /// UID sequence of messages that have been expunged final MessageSequence vanishedMessages; /// true when the vanished messages do not lead to updated sequence IDs diff --git a/lib/imap/mailbox.dart b/lib/imap/mailbox.dart index b4810dec..38e52a44 100644 --- a/lib/imap/mailbox.dart +++ b/lib/imap/mailbox.dart @@ -58,7 +58,7 @@ class Mailbox { isInbox || isDrafts || isSent || isJunk || isTrash || isArchive; Mailbox(); - Mailbox.setup(this.name, this.flags) { + Mailbox.setup(this.name, this.path, this.flags) { isMarked = hasFlag(MailboxFlag.marked); hasChildren = hasFlag(MailboxFlag.hasChildren); isSelected = hasFlag(MailboxFlag.select); @@ -69,6 +69,30 @@ class Mailbox { return flags.contains(flag); } + Mailbox getParent(List knownMailboxes, String separator, + {bool create = true, bool createIntermediate = true}) { + var lastSplitIndex = path.lastIndexOf(separator); + if (lastSplitIndex == -1) { + // this is a root mailbox, eg 'Inbox' + return null; + } + var parentPath = path.substring(0, lastSplitIndex); + var parent = knownMailboxes.firstWhere((box) => box.path == parentPath, + orElse: () => null); + if (parent == null && create) { + lastSplitIndex = parentPath.lastIndexOf(separator); + var parentName = lastSplitIndex == -1 + ? parentPath + : parentPath.substring(lastSplitIndex + 1); + parent = Mailbox.setup(parentName, parentPath, [MailboxFlag.noSelect]); + if ((lastSplitIndex != -1) && (!createIntermediate)) { + parent = parent.getParent(knownMailboxes, separator, + create: true, createIntermediate: false); + } + } + return parent; + } + @override String toString() { var buffer = StringBuffer()..write('"')..write(path)..write('"'); diff --git a/lib/mail/mail_account.dart b/lib/mail/mail_account.dart new file mode 100644 index 00000000..b751cbd3 --- /dev/null +++ b/lib/mail/mail_account.dart @@ -0,0 +1,26 @@ +import 'package:enough_mail/discover/client_config.dart'; +import 'package:enough_mail/enough_mail.dart'; + +import 'mail_authentication.dart'; + +class MailServerConfig { + ServerConfig serverConfig; + MailAuthentication authentication; + List serverCapabilities; + + String pathSeparator; + + bool supports(String capabilityName) { + return (serverCapabilities.firstWhere((c) => c.name == capabilityName, + orElse: () => null) != + null); + } +} + +class MailAccount { + String name; + String email; + MailServerConfig incoming; + MailServerConfig outgoing; + String outgoingClientDomain = 'enough.de'; +} diff --git a/lib/mail/mail_authentication.dart b/lib/mail/mail_authentication.dart new file mode 100644 index 00000000..2cd4ea00 --- /dev/null +++ b/lib/mail/mail_authentication.dart @@ -0,0 +1,36 @@ +import 'package:enough_mail/discover/client_config.dart'; +import 'package:enough_mail/imap/imap_client.dart'; +import 'package:enough_mail/pop/pop_client.dart'; +import 'package:enough_mail/smtp/smtp_client.dart'; + +import 'mail_response.dart'; + +abstract class MailAuthentication { + Future authenticate(ServerConfig serverConfig, + {ImapClient imap, PopClient pop, SmtpClient smtp}); +} + +class PlainAuthentication extends MailAuthentication { + String userName; + String password; + PlainAuthentication(this.userName, this.password); + + @override + Future authenticate(ServerConfig serverConfig, + {ImapClient imap, PopClient pop, SmtpClient smtp}) async { + switch (serverConfig.type) { + case ServerType.imap: + var imapResponse = await imap.login(userName, password); + return MailResponseHelper.createFromImap(imapResponse); + break; + case ServerType.pop: + var popResponse = await pop.login(userName, password); + return MailResponseHelper.createFromPop(popResponse); + break; + case ServerType.smtp: + break; + default: + throw StateError('Unknown server type ${serverConfig.typeName}'); + } + } +} diff --git a/lib/mail/mail_client.dart b/lib/mail/mail_client.dart new file mode 100644 index 00000000..c2a2b382 --- /dev/null +++ b/lib/mail/mail_client.dart @@ -0,0 +1,527 @@ +import 'dart:async'; + +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/mail/tree.dart'; +import 'package:event_bus/event_bus.dart'; + +import 'mail_account.dart'; +import 'mail_events.dart'; +import 'mail_response.dart'; + +/// Highlevel online API to access mail. +class MailClient { + static const Duration defaultPollingDuration = Duration(seconds: 30); + int _downloadSizeLimit; + final MailAccount _account; + EventBus get eventBus => _eventBus; + final EventBus _eventBus = EventBus(); + bool _isLogEnabled; + + Mailbox _selectedMailbox; + + List _mailboxes; + + SmtpClient _smtpClient; + _IncomingMailClient _incomingMailClient; + + /// Creates a new highlevel online mail client. + /// Specify the account settings with [account]. + /// Set [isLogEnabled] to true to debug connection issues. + /// Specify the optional [downloadSizeLimit] in bytes to only download messages automatically that are this size or lower. + MailClient(this._account, + {bool isLogEnabled = false, int downloadSizeLimit}) { + _isLogEnabled = isLogEnabled; + _downloadSizeLimit = downloadSizeLimit; + } + + //Future>> poll(Mailbox mailbox) {} + + /// Connects and authenticates with the specified incoming mail server. + Future connect() { + var config = _account.incoming; + if (config.serverConfig.type == ServerType.imap) { + _incomingMailClient = _IncomingImapClient( + _downloadSizeLimit, _eventBus, _isLogEnabled, config); + } else if (config.serverConfig.type == ServerType.pop) { + _incomingMailClient = _IncomingPopClient( + _downloadSizeLimit, _eventBus, _isLogEnabled, config); + } else { + throw StateError( + 'Unsupported incoming server type [${config.serverConfig.typeName}].'); + } + return _incomingMailClient.connect(); + } + + // Future tryAuthenticate( + // ServerConfig serverConfig, MailAuthentication authentication) { + // return authentication.authenticate(this, serverConfig); + // } + + /// Lists all mailboxes/folders of the incoming mail server. + Future>> listMailboxes() async { + var response = await _incomingMailClient.listMailboxes(); + _mailboxes = response.result; + return response; + } + + /// Lists all mailboxes/folders of the incoming mail server as a tree. + /// Optionally set [createIntermediate] to false, in case not all intermediate folders should be created, if not already present on the server. + Future>> listMailboxesAsTree( + {bool createIntermediate = true}) async { + var mailboxes = _mailboxes; + if (mailboxes == null) { + var flatResponse = await listMailboxes(); + if (flatResponse.isFailedStatus) { + return MailResponseHelper.failure>(flatResponse.errorId); + } + mailboxes = flatResponse.result; + } + var separator = _account.incoming.pathSeparator; + var tree = Tree(null); + tree.populateFromList( + mailboxes, + (child) => child.getParent(mailboxes, separator, + createIntermediate: createIntermediate)); + return MailResponseHelper.success>(tree); + } + + /// Shortcut to select the INBOX. + /// Optionally specify if CONDSTORE support should be enabled with [enableCondstore] - for IMAP servers that support CONDSTORE only. + /// Optionally specify quick resync parameters with [qresync] - for IMAP servers that support QRESYNC only. + Future> selectInbox( + {bool enableCondstore = false, QResyncParameters qresync}) async { + var mailboxes = _mailboxes; + if (mailboxes == null) { + var flatResponse = await listMailboxes(); + if (flatResponse.isFailedStatus) { + return MailResponseHelper.failure(flatResponse.errorId); + } + mailboxes = flatResponse.result; + } + var inbox = mailboxes.firstWhere((box) => box.isInbox, orElse: () => null); + inbox ??= mailboxes.firstWhere((box) => box.name.toLowerCase() == 'inbox', + orElse: () => null); + if (inbox == null) { + return MailResponseHelper.failure('inboxNotFound'); + } + return selectMailbox(inbox, + enableCondstore: enableCondstore, qresync: qresync); + } + + /// Selects the specified [mailbox]/folder. + /// Optionally specify if CONDSTORE support should be enabled with [enableCondstore]. + /// Optionally specify quick resync parameters with [qresync]. + Future> selectMailbox(Mailbox mailbox, + {bool enableCondstore = false, QResyncParameters qresync}) async { + var response = await _incomingMailClient.selectMailbox(mailbox, + enableCondstore: enableCondstore, qresync: qresync); + _selectedMailbox = response.result; + return response; + } + + /// Loads the specified segment of messages starting at the latest message and going down [count] messages. + /// Specify segment's number with [page] - by default this is 1, so the first segment is downloaded. + /// Optionally specify the [mailbox] in case none has been selected before or if another mailbox/folder should be queried. + Future>> fetchMessages( + {Mailbox mailbox, int count = 20, int page = 1}) async { + mailbox ??= _selectedMailbox; + if (mailbox == null) { + throw StateError('Either specify a mailbox or select a mailbox first'); + } + if (mailbox != _selectedMailbox) { + var selectResponse = await selectMailbox(mailbox); + if (selectResponse.isFailedStatus) { + return MailResponseHelper.failure>('select'); + } + mailbox = selectResponse.result; + } + return _incomingMailClient.fetchMessages( + mailbox: mailbox, count: count, page: page); + } + + Future _connectOutgoingIfRequired() async { + if (_smtpClient == null) { + _smtpClient ??= SmtpClient(_account.outgoingClientDomain, + bus: eventBus, isLogEnabled: _isLogEnabled); + var config = _account.outgoing.serverConfig; + var response = + await _smtpClient.connectToServer(config.hostname, config.port); + if (response.isFailedStatus) { + _smtpClient = null; + return MailResponseHelper.failure('smtp.connect'); + } + return _account.outgoing.authentication.authenticate(config); + } + return Future.value(MailResponseHelper.success(null)); + } + + /// Sends the specified message. + /// Use [MessageBuilder] to create new messages. + Future sendMessage(MimeMessage message) async { + if (_smtpClient == null) { + var response = await _connectOutgoingIfRequired(); + if (response.isFailedStatus) { + _smtpClient = null; + return response; + } + } + var sendResponse = await _smtpClient.sendMessage(message); + if (sendResponse.isFailedStatus) { + return MailResponseHelper.failure('smtp.send'); + } + return MailResponseHelper.success(sendResponse.code); + } + + /// Starts listening for new incoming messages. + /// Listen for [MailFetchEvent] on the [eventBus] to get notified about new messages. + void startPolling([Duration duration = defaultPollingDuration]) { + _incomingMailClient.startPolling(duration); + } + + /// Stops listening for new messages. + void stopPolling() { + _incomingMailClient.stopPolling(); + } +} + +abstract class _IncomingMailClient { + int downloadSizeLimit; + + bool _isLogEnabled; + EventBus _eventBus; + final MailServerConfig _config; + Mailbox _selectedMailbox; + bool _isPollingStopRequested; + + _IncomingMailClient(this.downloadSizeLimit, this._config); + + Future connect(); + + Future>> listMailboxes(); + + Future> selectMailbox(Mailbox mailbox, + {bool enableCondstore = false, QResyncParameters qresync}); + + Future>> fetchMessages( + {Mailbox mailbox, int count = 20, int page = 1}); + + Future>> fetchMessageSequence( + MessageSequence sequence, bool isUidSequence); + + Future> fetchMessage(int id, bool isUid); + + Future>> poll(); + + void startPolling(Duration duration) { + _isPollingStopRequested = false; + Timer.periodic(duration, _poll); + } + + void stopPolling() { + _isPollingStopRequested = true; + } + + void _poll(Timer timer) { + if (_isPollingStopRequested) { + timer.cancel(); + } else { + poll(); + } + } +} + +class _IncomingImapClient extends _IncomingMailClient { + ImapClient _imapClient; + bool _isQResyncEnabled = false; + bool _supportsIdle = false; + final List _fetchMessages = []; + + _IncomingImapClient(int downloadSizeLimit, EventBus eventBus, + bool isLogEnabled, MailServerConfig config) + : super(downloadSizeLimit, config) { + _imapClient = ImapClient(bus: eventBus, isLogEnabled: isLogEnabled); + _eventBus = eventBus; + _isLogEnabled = isLogEnabled; + eventBus.on().listen(_onImapEvent); + } + + void _onImapEvent(ImapEvent event) async { + //print('imap event: ${event.eventType}'); + switch (event.eventType) { + case ImapEventType.fetch: + var message = (event as ImapFetchEvent).message; + MailResponse response; + if (message.uid != null) { + response = await fetchMessage(message.uid, true); + } else { + response = await fetchMessage(message.sequenceId, false); + } + if (response.isOkStatus) { + message = response.result; + } + _eventBus.fire(MailLoadEvent(message)); + _fetchMessages.add(message); + break; + case ImapEventType.exists: + var evt = event as ImapMessagesExistEvent; + var sequence = MessageSequence(); + if (evt.newMessagesExists - evt.oldMessagesExists > 1) { + sequence.addRange(evt.oldMessagesExists, evt.newMessagesExists); + } else { + sequence.add(evt.newMessagesExists); + } + var response = await fetchMessageSequence(sequence, false); + if (response.isOkStatus) { + for (var message in response.result) { + _eventBus.fire(MailLoadEvent(message)); + _fetchMessages.add(message); + } + } + break; + case ImapEventType.vanished: + var evt = event as ImapVanishedEvent; + _eventBus.fire(MailVanishedEvent(evt.vanishedMessages, evt.isEarlier)); + break; + case ImapEventType.expunge: + //TODO handle EXPUNGE + break; + case ImapEventType.connectionLost: + await connect(); + } + } + + @override + Future connect() async { + _imapClient ??= ImapClient(bus: _eventBus, isLogEnabled: _isLogEnabled); + var serverConfig = _config.serverConfig; + await _imapClient.connectToServer(serverConfig.hostname, serverConfig.port, + isSecure: serverConfig.socketType == SocketType.ssl); + var response = await _config.authentication + .authenticate(_config.serverConfig, imap: _imapClient); + if (response.isOkStatus) { + //TODO compare with previous capabilities and possibly fire events for new or removed server capabilities + _config.serverCapabilities = _imapClient.serverInfo.capabilities; + if (_config.supports('QRESYNC')) { + var enabledResponse = await _imapClient.enable(['QRESYNC']); + _isQResyncEnabled = enabledResponse.isOkStatus; + } + _supportsIdle = _config.supports('IDLE'); + } + return response; + } + + @override + Future>> listMailboxes() async { + var mailboxResponse = await _imapClient.listMailboxes(recursive: true); + if (mailboxResponse.isFailedStatus) { + var errorId = 'list'; + return MailResponseHelper.failure>(errorId); + } + var separator = _imapClient.serverInfo.pathSeparator; + _config.pathSeparator = separator; + return MailResponseHelper.createFromImap>(mailboxResponse); + } + + @override + Future> selectMailbox(Mailbox mailbox, + {bool enableCondstore = false, QResyncParameters qresync}) async { + if (_selectedMailbox != null) { + await _imapClient.closeMailbox(); + } + if (qresync == null && + _isQResyncEnabled && + mailbox.highestModSequence != null) { + qresync = + QResyncParameters(mailbox.uidValidity, mailbox.highestModSequence); + } + var imapResponse = await _imapClient.selectMailbox(mailbox, + enableCondStore: enableCondstore, qresync: qresync); + _selectedMailbox = imapResponse.result; + return MailResponseHelper.createFromImap(imapResponse); + } + + @override + Future>> fetchMessages( + {Mailbox mailbox, int count = 20, int page = 1}) { + var sequence = MessageSequence.fromAll(); + if (count != null) { + var end = mailbox.messagesExists; + if (page != null) { + end -= page * count; + } + var start = end - count; + if (start < 1) { + start = 1; + } + sequence = MessageSequence.fromRange(start, end); + } + return fetchMessageSequence(sequence, false); + } + + @override + Future>> fetchMessageSequence( + MessageSequence sequence, bool isUidSequence) async { + String criteria; + if (downloadSizeLimit != null) { + criteria = 'UID RFC822.SIZE ENVELOPE'; + } else { + criteria = 'BODY.PEEK[]'; + } + var response = isUidSequence + ? await _imapClient.uidFetchMessages(sequence, criteria) + : await _imapClient.fetchMessages(sequence, criteria); + if (response.isFailedStatus) { + return MailResponseHelper.failure>('fetch'); + } + if (response.result.vanishedMessagesUidSequence?.isNotEmpty() ?? false) { + _eventBus.fire(MailVanishedEvent( + response.result.vanishedMessagesUidSequence, false)); + } + if (downloadSizeLimit != null) { + var smallEnoughMessages = + response.result.messages.where((msg) => msg.size < downloadSizeLimit); + sequence = MessageSequence(); + for (var msg in smallEnoughMessages) { + sequence.add(msg.uid); + } + response = await _imapClient.fetchMessages(sequence, 'BODY.PEEK[]'); + if (response.isFailedStatus) { + return MailResponseHelper.failure>('fetch'); + } + } + return MailResponseHelper.success>( + response.result.messages); + } + + @override + Future>> poll() async { + _fetchMessages.clear(); + await _imapClient.noop(); + if (_fetchMessages.isEmpty) { + return MailResponseHelper.failure(null); + } + return MailResponseHelper.success>( + _fetchMessages.toList()); + } + + @override + Future> fetchMessage(int id, bool isUid) async { + var sequence = MessageSequence.fromId(id); + var response = await fetchMessageSequence(sequence, isUid); + if (response.isOkStatus) { + return MailResponseHelper.success(response.result.first); + } else { + return MailResponseHelper.failure(response.errorId); + } + } + + @override + void startPolling(Duration duration) { + // if (_supportsIdle) { + // _imapClient.idleStart() + //TODO support IDLE + // } else { + super.startPolling(duration); + // } + } +} + +class _IncomingPopClient extends _IncomingMailClient { + List _popMessageListing; + final Mailbox _popInbox = + Mailbox.setup('Inbox', 'Inbox', [MailboxFlag.inbox]); + + PopClient _popClient; + _IncomingPopClient(int downloadSizeLimit, EventBus eventBus, + bool isLogEnabled, MailServerConfig config) + : super(downloadSizeLimit, config) { + config = config; + _popClient = PopClient(bus: eventBus, isLogEnabled: isLogEnabled); + _eventBus = eventBus; + _isLogEnabled = isLogEnabled; + } + + @override + Future connect() async { + var serverConfig = _config.serverConfig; + await _popClient.connectToServer(serverConfig.hostname, serverConfig.port, + isSecure: serverConfig.socketType == SocketType.ssl); + var authResponse = await _config.authentication + .authenticate(_config.serverConfig, pop: _popClient); + + return authResponse; + } + + @override + Future>> listMailboxes() { + _config.pathSeparator = '/'; + var response = MailResponseHelper.success>([_popInbox]); + return Future.value(response); + } + + @override + Future> selectMailbox(Mailbox mailbox, + {bool enableCondstore = false, QResyncParameters qresync}) async { + if (mailbox != _popInbox) { + throw StateError('Unknown mailbox $mailbox'); + } + var statusResponse = await _popClient.status(); + if (statusResponse.isFailedStatus) { + return MailResponseHelper.failure('status'); + } + mailbox.messagesExists = statusResponse.result.numberOfMessages; + _selectedMailbox = mailbox; + return MailResponseHelper.success(mailbox); + } + + @override + Future>> fetchMessages( + {Mailbox mailbox, int count = 20, int page = 1}) async { + if (_popMessageListing == null) { + var messageListResponse = await _popClient.list(); + if (messageListResponse.isFailedStatus) { + return MailResponseHelper.failure('fetch'); + } + _popMessageListing = messageListResponse.result; + } + var listings = _popMessageListing; + if (count != null) { + var startIndex = _popMessageListing.length - count; + if (page != null) { + startIndex -= page * count; + } + if (startIndex < 0) { + count += startIndex; + startIndex = 0; + } + listings = listings.sublist(startIndex, startIndex + count); + } + var messages = []; + for (var listing in listings) { + //TODO check listing.sizeInBytes + var messageResponse = await _popClient.retrieve(listing.id); + if (messageResponse.isOkStatus) { + messages.add(messageResponse.result); + } + } + return MailResponseHelper.success>(messages); + } + + @override + Future> fetchMessage(int id, bool isUid) async { + throw UnimplementedError(); + } + + @override + Future>> poll() { + // TODO: implement poll + throw UnimplementedError(); + } + + @override + Future>> fetchMessageSequence( + MessageSequence sequence, bool isUidSequence) { + // TODO: implement fetchMessageSequence + throw UnimplementedError(); + } +} diff --git a/lib/mail/mail_events.dart b/lib/mail/mail_events.dart new file mode 100644 index 00000000..558634fc --- /dev/null +++ b/lib/mail/mail_events.dart @@ -0,0 +1,31 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/mime_message.dart'; + +/// Classification of Mail events +/// +/// Compare [MailEvent] +enum MailEventType { newMail, vanished } + +/// Base class for any event that can be fired by the MailClient at any time. +/// Compare [MailClient.eventBus] +class MailEvent { + final MailEventType eventType; + MailEvent(this.eventType); +} + +/// Notifies about a message that has been deleted +class MailLoadEvent extends MailEvent { + final MimeMessage message; + MailLoadEvent(this.message) : super(MailEventType.newMail); +} + +/// Notifies about the UIDs of removed messages +class MailVanishedEvent extends MailEvent { + /// UID sequence of messages that have been expunged + final MessageSequence sequence; + + /// true when the vanished messages do not lead to updated sequence IDs + final bool isEarlier; + MailVanishedEvent(this.sequence, this.isEarlier) + : super(MailEventType.vanished); +} diff --git a/lib/mail/mail_response.dart b/lib/mail/mail_response.dart new file mode 100644 index 00000000..a6442d1d --- /dev/null +++ b/lib/mail/mail_response.dart @@ -0,0 +1,35 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/imap/response.dart'; + +class MailResponse { + T result; + bool isOkStatus; + bool get isFailedStatus => !isOkStatus; + String errorId; +} + +class MailResponseHelper { + static MailResponse createFromImap(Response imap) { + return MailResponse() + ..isOkStatus = imap.isOkStatus + ..result = imap.result; + } + + static MailResponse createFromPop(PopResponse popResponse) { + return MailResponse() + ..isOkStatus = popResponse.isOkStatus + ..result = popResponse.result; + } + + static MailResponse success(T result) { + return MailResponse() + ..result = result + ..isOkStatus = true; + } + + static MailResponse failure(String errorId) { + return MailResponse() + ..errorId + ..isOkStatus = false; + } +} diff --git a/lib/mail/tree.dart b/lib/mail/tree.dart new file mode 100644 index 00000000..39c7187f --- /dev/null +++ b/lib/mail/tree.dart @@ -0,0 +1,111 @@ +class Tree { + TreeElement root; + + Tree(T rootValue) { + root = TreeElement(rootValue, null); + } + + @override + String toString() { + return root.toString(); + } + + /// Lists all leafs of this tree + /// Specify how to detect the leafs with [isLeaf]. + List flatten(bool Function(T element) isLeaf) { + var leafs = []; + _addLeafs(root, isLeaf, leafs); + return leafs; + } + + void _addLeafs( + TreeElement root, bool Function(T element) isLeaf, List leafs) { + for (var child in root.children) { + if (isLeaf == null || isLeaf(child.value)) { + leafs.add(child.value); + } + if (child.children != null) { + _addLeafs(child, isLeaf, leafs); + } + } + } + + void populateFromList(List elements, T Function(T child) getParent) { + for (var element in elements) { + var parent = getParent(element); + if (parent == null) { + root.addChild(element); + } else { + _addChildToParent(element, parent, getParent); + } + } + } + + TreeElement _addChildToParent( + T child, T parent, T Function(T child) getParent) { + var treeElement = locate(parent); + if (treeElement == null) { + var grandParent = getParent(parent); + if (grandParent == null) { + // add new tree element to root: + treeElement = root.addChild(parent); + } else { + treeElement = _addChildToParent(parent, grandParent, getParent); + } + } + return treeElement.addChild(child); + } + + TreeElement locate(T value) { + return _locate(value, root); + } + + TreeElement _locate(T value, TreeElement root) { + for (var child in root.children) { + if (child.value == value) { + return child; + } + if (child.hasChildren) { + var result = _locate(value, child); + if (result != null) { + return result; + } + } + } + return null; + } +} + +class TreeElement { + T value; + List> children; + bool get hasChildren => children != null && children.isNotEmpty; + TreeElement parent; + + TreeElement(this.value, this.parent); + + TreeElement addChild(T child) { + children ??= >[]; + var element = TreeElement(child, this); + children.add(element); + return element; + } + + @override + String toString() { + var buffer = StringBuffer(); + render(buffer); + return buffer.toString(); + } + + void render(StringBuffer buffer, [String padding = '']) { + buffer..write(padding)..write(value)..write('\n'); + if (children != null) { + buffer..write(padding)..write('[\n'); + for (var child in children) { + child.render(buffer, padding + ' '); + } + buffer..write(padding)..write(']\n'); + } + } +} diff --git a/lib/pop/pop_client.dart b/lib/pop/pop_client.dart index 360a8b5f..f5625bf5 100644 --- a/lib/pop/pop_client.dart +++ b/lib/pop/pop_client.dart @@ -28,7 +28,8 @@ class PopClient { /// _log(event.type); /// }); /// ``` - EventBus eventBus; + EventBus get eventBus => _eventBus; + EventBus _eventBus; bool _isSocketClosingExpected = false; bool get isLoggedIn => _isLoggedIn; bool get isNotLoggedIn => !_isLoggedIn; @@ -45,7 +46,7 @@ class PopClient { PopClient({EventBus bus, bool isLogEnabled = false}) { bus ??= EventBus(); - eventBus = bus; + _eventBus = bus; _isLogEnabled = isLogEnabled; } diff --git a/lib/smtp/smtp_client.dart b/lib/smtp/smtp_client.dart index 629edf5f..dc24c5f7 100644 --- a/lib/smtp/smtp_client.dart +++ b/lib/smtp/smtp_client.dart @@ -42,7 +42,8 @@ class SmtpClient { /// _log(event.type); /// }); /// ``` - EventBus eventBus; + EventBus get eventBus => _eventBus; + EventBus _eventBus; bool _isSocketClosingExpected = false; bool get isLoggedIn => _isLoggedIn; @@ -63,7 +64,7 @@ class SmtpClient { SmtpClient(String clientDomain, {EventBus bus, bool isLogEnabled = false}) { _clientDomain = clientDomain; bus ??= EventBus(); - eventBus = bus; + _eventBus = bus; _isLogEnabled = isLogEnabled; } diff --git a/lib/src/imap/list_parser.dart b/lib/src/imap/list_parser.dart index 1d351667..e0db5f3e 100644 --- a/lib/src/imap/list_parser.dart +++ b/lib/src/imap/list_parser.dart @@ -126,6 +126,9 @@ class ListParser extends ResponseParser> { listDetails = listDetails.substring(endOfPathSeparatorIndex + 2); } } + if (listDetails.startsWith('"')) { + listDetails = listDetails.substring(1, listDetails.length - 1); + } box.path = listDetails; var lastPathSeparatorIndex = listDetails.lastIndexOf(info.pathSeparator, listDetails.length - 2); diff --git a/lib/src/imap/noop_parser.dart b/lib/src/imap/noop_parser.dart index 94fa89ee..362af038 100644 --- a/lib/src/imap/noop_parser.dart +++ b/lib/src/imap/noop_parser.dart @@ -1,5 +1,5 @@ import 'package:enough_mail/enough_mail.dart'; -import 'package:enough_mail/imap/events.dart'; +import 'package:enough_mail/imap/imap_events.dart'; import 'package:enough_mail/imap/mailbox.dart'; import 'package:enough_mail/imap/response.dart'; import 'package:enough_mail/src/imap/select_parser.dart'; diff --git a/lib/src/imap/select_parser.dart b/lib/src/imap/select_parser.dart index 3e7aa185..27027128 100644 --- a/lib/src/imap/select_parser.dart +++ b/lib/src/imap/select_parser.dart @@ -1,4 +1,4 @@ -import 'package:enough_mail/imap/events.dart'; +import 'package:enough_mail/imap/imap_events.dart'; import 'package:enough_mail/imap/mailbox.dart'; import 'package:enough_mail/imap/response.dart'; import 'package:enough_mail/src/imap/all_parsers.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 10396856..fa2631de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: intl: ^0.16.1 crypto: ^2.1.5 basic_utils: ^2.5.3 - xml: ^4.2.0 + xml: ^3.6.1 dev_dependencies: diff --git a/test/imap/imap_client_test.dart b/test/imap/imap_client_test.dart index 155deeab..58f3dd51 100644 --- a/test/imap/imap_client_test.dart +++ b/test/imap/imap_client_test.dart @@ -1269,7 +1269,7 @@ void main() { expect(logoutResponse.status, ResponseStatus.OK); //await Future.delayed(Duration(seconds: 1)); - await client.close(); + await client.closeConnection(); _log('done connecting'); client = null; }); diff --git a/test/imap/mock_imap_server.dart b/test/imap/mock_imap_server.dart index 01f3c795..e5e2c522 100644 --- a/test/imap/mock_imap_server.dart +++ b/test/imap/mock_imap_server.dart @@ -14,7 +14,7 @@ class ServerMailbox extends Mailbox { ServerMailbox(String name, List flags, String messageFlags, String permanentMessageFlags) - : super.setup(name, flags) { + : super.setup(name, name, flags) { super.messageFlags = messageFlags.split(' '); super.permanentMessageFlags = permanentMessageFlags.split(' '); }