From 33e66b7ed36ad88e0efa338897188e4c37224ec3 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Tue, 2 Jun 2020 21:38:57 +0200 Subject: [PATCH] Provide POP3 support, close #63 --- CHANGELOG.md | 3 + README.md | 93 +++++-- example/enough_mail_example.dart | 89 ++++-- lib/enough_mail.dart | 4 + lib/pop/pop_client.dart | 254 ++++++++++++++++++ lib/pop/pop_events.dart | 11 + lib/pop/pop_response.dart | 22 ++ lib/smtp/smtp_client.dart | 8 +- lib/src/pop/commands/all_commands.dart | 13 + lib/src/pop/commands/pop_apop_command.dart | 15 ++ lib/src/pop/commands/pop_connect_command.dart | 10 + lib/src/pop/commands/pop_delete_command.dart | 5 + lib/src/pop/commands/pop_list_command.dart | 9 + lib/src/pop/commands/pop_noop_command.dart | 5 + lib/src/pop/commands/pop_pass_command.dart | 5 + lib/src/pop/commands/pop_quit_command.dart | 14 + lib/src/pop/commands/pop_reset_command.dart | 5 + .../pop/commands/pop_retrieve_command.dart | 9 + .../pop/commands/pop_starttls_command.dart | 6 + lib/src/pop/commands/pop_status_command.dart | 7 + lib/src/pop/commands/pop_top_command.dart | 9 + lib/src/pop/commands/pop_uidl_command.dart | 9 + lib/src/pop/commands/pop_user_command.dart | 5 + lib/src/pop/parsers/all_parsers.dart | 6 + .../pop/parsers/pop_connection_parser.dart | 23 ++ lib/src/pop/parsers/pop_list_parser.dart | 33 +++ lib/src/pop/parsers/pop_retrieve_parser.dart | 27 ++ lib/src/pop/parsers/pop_standard_parser.dart | 12 + lib/src/pop/parsers/pop_status_parser.dart | 23 ++ lib/src/pop/parsers/pop_uidl_parser.dart | 33 +++ lib/src/pop/pop_command.dart | 31 +++ lib/src/pop/pop_response_parser.dart | 12 + lib/src/util/ascii_runes.dart | 1 + lib/src/util/uint8_list_reader.dart | 32 +++ pubspec.yaml | 5 +- 35 files changed, 800 insertions(+), 48 deletions(-) create mode 100644 lib/pop/pop_client.dart create mode 100644 lib/pop/pop_events.dart create mode 100644 lib/pop/pop_response.dart create mode 100644 lib/src/pop/commands/all_commands.dart create mode 100644 lib/src/pop/commands/pop_apop_command.dart create mode 100644 lib/src/pop/commands/pop_connect_command.dart create mode 100644 lib/src/pop/commands/pop_delete_command.dart create mode 100644 lib/src/pop/commands/pop_list_command.dart create mode 100644 lib/src/pop/commands/pop_noop_command.dart create mode 100644 lib/src/pop/commands/pop_pass_command.dart create mode 100644 lib/src/pop/commands/pop_quit_command.dart create mode 100644 lib/src/pop/commands/pop_reset_command.dart create mode 100644 lib/src/pop/commands/pop_retrieve_command.dart create mode 100644 lib/src/pop/commands/pop_starttls_command.dart create mode 100644 lib/src/pop/commands/pop_status_command.dart create mode 100644 lib/src/pop/commands/pop_top_command.dart create mode 100644 lib/src/pop/commands/pop_uidl_command.dart create mode 100644 lib/src/pop/commands/pop_user_command.dart create mode 100644 lib/src/pop/parsers/all_parsers.dart create mode 100644 lib/src/pop/parsers/pop_connection_parser.dart create mode 100644 lib/src/pop/parsers/pop_list_parser.dart create mode 100644 lib/src/pop/parsers/pop_retrieve_parser.dart create mode 100644 lib/src/pop/parsers/pop_standard_parser.dart create mode 100644 lib/src/pop/parsers/pop_status_parser.dart create mode 100644 lib/src/pop/parsers/pop_uidl_parser.dart create mode 100644 lib/src/pop/pop_command.dart create mode 100644 lib/src/pop/pop_response_parser.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index abecd6cf..a56684bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.23 +- Provide [POP3](https://tools.ietf.org/html/rfc1939) support + ## 0.0.22 - Breaking API change: use FETCH IMAP methods now return `FetchImapResult` instead of `List` - Breaking API change: `ImapFetchEvent` now contains a full `MimeMessage` instead of just the sequence ID and flags diff --git a/README.md b/README.md index b030771c..5da8941d 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,30 @@ A simple usage example: import 'dart:io'; import 'package:enough_mail/enough_mail.dart'; +String userName = 'user.name'; +String password = 'password'; +String imapServerHost = 'imap.domain.com'; +int imapServerPort = 993; +bool isImapServerSecure = true; +String popServerHost = 'pop.domain.com'; +int popServerPort = 995; +bool isPopServerSecure = true; +String smtpServerHost = 'smtp.domain.com'; +int smtpServerPort = 465; +bool isSmtpServerSecure = true; + void main() async { await imapExample(); await smtpExample(); + await popExample(); exit(0); } Future imapExample() async { var client = ImapClient(isLogEnabled: false); - await client.connectToServer('imap.domain.com', 993, isSecure: true); - var loginResponse = await client.login('user.name', 'password'); + await client.connectToServer(imapServerHost, imapServerPort, + isSecure: isImapServerSecure); + var loginResponse = await client.login(userName, password); if (loginResponse.isOkStatus) { var listResponse = await client.listMailboxes(); if (listResponse.isOkStatus) { @@ -34,23 +48,7 @@ Future imapExample() async { if (fetchResponse.isOkStatus) { var messages = fetchResponse.result.messages; for (var message in messages) { - print( - 'from: ${message.from} with subject "${message.decodeSubject()}"'); - if (!message.isTextPlainMessage()) { - print(' content-type: ${message.mediaType}'); - } else { - var plainText = message.decodeTextPlainPart(); - if (plainText != null) { - var lines = plainText.split('\r\n'); - for (var line in lines) { - if (line.startsWith('>')) { - // break when quoted text starts - break; - } - print(line); - } - } - } + printMessage(message); } } } @@ -60,7 +58,8 @@ Future imapExample() async { Future smtpExample() async { var client = SmtpClient('enough.de', isLogEnabled: true); - await client.connectToServer('smtp.domain.com', 465, isSecure: true); + await client.connectToServer(smtpServerHost, smtpServerPort, + isSecure: isSmtpServerSecure); var ehloResponse = await client.ehlo(); if (!ehloResponse.isOkStatus) { print('SMTP: unable to say helo/ehlo: ${ehloResponse.message}'); @@ -79,6 +78,56 @@ Future smtpExample() async { print('message sent: ${sendResponse.isOkStatus}'); } } + +Future popExample() async { + var client = PopClient(isLogEnabled: false); + await client.connectToServer(popServerHost, popServerPort, + isSecure: isPopServerSecure); + var loginResponse = await client.login(userName, password); + //var loginResponse = await client.loginWithApop(userName, password); // optional different login mechanism + if (loginResponse.isOkStatus) { + var statusResponse = await client.status(); + if (statusResponse.isOkStatus) { + print( + 'status: messages count=${statusResponse.result.numberOfMessages}, messages size=${statusResponse.result.totalSizeInBytes}'); + var listResponse = + await client.list(statusResponse.result.numberOfMessages); + print( + 'last message: id=${listResponse.result?.first?.id} size=${listResponse.result?.first?.sizeInBytes}'); + var retrieveResponse = + await client.retrieve(statusResponse.result.numberOfMessages); + if (retrieveResponse.isOkStatus) { + printMessage(retrieveResponse.result); + } else { + print('last message could not be retrieved'); + } + retrieveResponse = + await client.retrieve(statusResponse.result.numberOfMessages + 1); + print( + 'trying to retrieve newer message succeeded: ${retrieveResponse.isOkStatus}'); + } + } + await client.quit(); +} + +void printMessage(MimeMessage message) { + print('from: ${message.from} with subject "${message.decodeSubject()}"'); + if (!message.isTextPlainMessage()) { + print(' content-type: ${message.mediaType}'); + } else { + var plainText = message.decodeTextPlainPart(); + if (plainText != null) { + var lines = plainText.split('\r\n'); + for (var line in lines) { + if (line.startsWith('>')) { + // break when quoted text starts + break; + } + print(line); + } + } + } +} ``` ## Installation @@ -86,7 +135,7 @@ Add this dependency your pubspec.yaml file: ``` dependencies: - enough_mail: ^0.0.22 + enough_mail: ^0.0.23 ``` The latest version or `enough_mail` is [![enough_mail version](https://img.shields.io/pub/v/enough_mail.svg)](https://pub.dartlang.org/packages/enough_mail). @@ -102,6 +151,7 @@ Want to contribute? Please check out [contribute](https://github.com/Enough-Soft ### Done * ✅ [IMAP4 rev1](https://tools.ietf.org/html/rfc3501) support * ✅ basic [SMTP](https://tools.ietf.org/html/rfc5321) support +* ✅ [POP3](https://tools.ietf.org/html/rfc1939) support * ✅ [MIME](https://tools.ietf.org/html/rfc2045) parsing and generation support The following IMAP extensions are supported: @@ -132,7 +182,6 @@ Transfer encodings: * support [Message Preview Generation](https://datatracker.ietf.org/doc/draft-ietf-extra-imap-fetch-preview/) * support [WebPush IMAP Extension](https://github.com/coi-dev/coi-specs/blob/master/webpush-spec.md) * support [Open PGP](https://tools.ietf.org/html/rfc4880) -* support [POP3](https://tools.ietf.org/html/rfc1939) ### Develop and Contribute * To start check out the package and then run `pub run test` to run all tests. diff --git a/example/enough_mail_example.dart b/example/enough_mail_example.dart index e9d2b712..dba903c8 100644 --- a/example/enough_mail_example.dart +++ b/example/enough_mail_example.dart @@ -1,16 +1,30 @@ import 'dart:io'; import 'package:enough_mail/enough_mail.dart'; +String userName = 'user.name'; +String password = 'password'; +String imapServerHost = 'imap.domain.com'; +int imapServerPort = 993; +bool isImapServerSecure = true; +String popServerHost = 'pop.domain.com'; +int popServerPort = 995; +bool isPopServerSecure = true; +String smtpServerHost = 'smtp.domain.com'; +int smtpServerPort = 465; +bool isSmtpServerSecure = true; + void main() async { await imapExample(); await smtpExample(); + await popExample(); exit(0); } Future imapExample() async { var client = ImapClient(isLogEnabled: false); - await client.connectToServer('imap.domain.com', 993, isSecure: true); - var loginResponse = await client.login('user.name', 'password'); + await client.connectToServer(imapServerHost, imapServerPort, + isSecure: isImapServerSecure); + var loginResponse = await client.login(userName, password); if (loginResponse.isOkStatus) { var listResponse = await client.listMailboxes(); if (listResponse.isOkStatus) { @@ -24,23 +38,7 @@ Future imapExample() async { if (fetchResponse.isOkStatus) { var messages = fetchResponse.result.messages; for (var message in messages) { - print( - 'from: ${message.from} with subject "${message.decodeSubject()}"'); - if (!message.isTextPlainMessage()) { - print(' content-type: ${message.mediaType}'); - } else { - var plainText = message.decodeTextPlainPart(); - if (plainText != null) { - var lines = plainText.split('\r\n'); - for (var line in lines) { - if (line.startsWith('>')) { - // break when quoted text starts - break; - } - print(line); - } - } - } + printMessage(message); } } } @@ -50,7 +48,8 @@ Future imapExample() async { Future smtpExample() async { var client = SmtpClient('enough.de', isLogEnabled: true); - await client.connectToServer('smtp.domain.com', 465, isSecure: true); + await client.connectToServer(smtpServerHost, smtpServerPort, + isSecure: isSmtpServerSecure); var ehloResponse = await client.ehlo(); if (!ehloResponse.isOkStatus) { print('SMTP: unable to say helo/ehlo: ${ehloResponse.message}'); @@ -69,3 +68,53 @@ Future smtpExample() async { print('message sent: ${sendResponse.isOkStatus}'); } } + +Future popExample() async { + var client = PopClient(isLogEnabled: false); + await client.connectToServer(popServerHost, popServerPort, + isSecure: isPopServerSecure); + var loginResponse = await client.login(userName, password); + //var loginResponse = await client.loginWithApop(userName, password); // optional different login mechanism + if (loginResponse.isOkStatus) { + var statusResponse = await client.status(); + if (statusResponse.isOkStatus) { + print( + 'status: messages count=${statusResponse.result.numberOfMessages}, messages size=${statusResponse.result.totalSizeInBytes}'); + var listResponse = + await client.list(statusResponse.result.numberOfMessages); + print( + 'last message: id=${listResponse.result?.first?.id} size=${listResponse.result?.first?.sizeInBytes}'); + var retrieveResponse = + await client.retrieve(statusResponse.result.numberOfMessages); + if (retrieveResponse.isOkStatus) { + printMessage(retrieveResponse.result); + } else { + print('last message could not be retrieved'); + } + retrieveResponse = + await client.retrieve(statusResponse.result.numberOfMessages + 1); + print( + 'trying to retrieve newer message succeeded: ${retrieveResponse.isOkStatus}'); + } + } + await client.quit(); +} + +void printMessage(MimeMessage message) { + print('from: ${message.from} with subject "${message.decodeSubject()}"'); + if (!message.isTextPlainMessage()) { + print(' content-type: ${message.mediaType}'); + } else { + var plainText = message.decodeTextPlainPart(); + if (plainText != null) { + var lines = plainText.split('\r\n'); + for (var line in lines) { + if (line.startsWith('>')) { + // break when quoted text starts + break; + } + print(line); + } + } + } +} diff --git a/lib/enough_mail.dart b/lib/enough_mail.dart index 6b72782c..a1c3be6c 100644 --- a/lib/enough_mail.dart +++ b/lib/enough_mail.dart @@ -8,6 +8,10 @@ export 'imap/message_sequence.dart'; export 'imap/metadata.dart'; export 'imap/qresync.dart'; +export 'pop/pop_client.dart'; +export 'pop/pop_events.dart'; +export 'pop/pop_response.dart'; + export 'smtp/smtp_client.dart'; export 'smtp/smtp_response.dart'; export 'smtp/smtp_events.dart'; diff --git a/lib/pop/pop_client.dart b/lib/pop/pop_client.dart new file mode 100644 index 00000000..360a8b5f --- /dev/null +++ b/lib/pop/pop_client.dart @@ -0,0 +1,254 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/pop/pop_events.dart'; +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/commands/all_commands.dart'; +import 'package:enough_mail/src/pop/parsers/pop_standard_parser.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; +import 'package:enough_mail/src/util/uint8_list_reader.dart'; +import 'package:event_bus/event_bus.dart'; + +/// Client to access POP3 compliant servers. +/// Compare https://tools.ietf.org/html/rfc1939 for details. +class PopClient { + /// Allows to listens for events + /// + /// If no event bus is specified in the constructor, an aysnchronous bus is used. + /// Usage: + /// ``` + /// eventBus.on().listen((event) { + /// // All events are of type SmtpConnectionLostEvent (or subtypes of it). + /// _log(event.type); + /// }); + /// + /// eventBus.on().listen((event) { + /// // All events are of type SmtpEvent (or subtypes of it). + /// _log(event.type); + /// }); + /// ``` + EventBus eventBus; + bool _isSocketClosingExpected = false; + bool get isLoggedIn => _isLoggedIn; + bool get isNotLoggedIn => !_isLoggedIn; + + bool _isLoggedIn = false; + Socket _socket; + final Uint8ListReader _uint8listReader = Uint8ListReader(); + bool _isLogEnabled; + PopCommand _currentCommand; + String _currentFirstResponseLine; + final PopStandardParser _standardParser = PopStandardParser(); + PopServerInfo _serverInfo; + set serverInfo(PopServerInfo info) => _serverInfo = info; + + PopClient({EventBus bus, bool isLogEnabled = false}) { + bus ??= EventBus(); + eventBus = bus; + _isLogEnabled = isLogEnabled; + } + + /// Connects to the specified server. + /// + /// Specify [isSecure] if you do not want to connect to a secure service. + Future connectToServer(String host, int port, + {bool isSecure = true}) async { + _log('connecting to server $host:$port - secure: $isSecure'); + var cmd = PopConnectCommand(this); + _currentCommand = cmd; + var socket = isSecure + ? await SecureSocket.connect(host, port) + : await Socket.connect(host, port); + connect(socket); + return cmd.completer.future; + } + + /// Starts to liste on [socket]. + /// + /// This is mainly useful for testing purposes, ensure to set [serverInfo] manually in this case. + void connect(Socket socket) { + socket.listen(onData, onDone: () { + _log('Done, connection closed'); + _isLoggedIn = false; + if (!_isSocketClosingExpected) { + eventBus.fire(PopConnectionLostEvent()); + } + }, onError: (error) { + _log('Error: $error'); + _isLoggedIn = false; + if (!_isSocketClosingExpected) { + eventBus.fire(PopConnectionLostEvent()); + } + }); + _isSocketClosingExpected = false; + _socket = socket; + } + + void onData(Uint8List data) { + //print('onData: [${String.fromCharCodes(data).replaceAll("\r\n", "\n")}]'); + _uint8listReader.add(data); + if (_currentFirstResponseLine == null) { + _currentFirstResponseLine = _uint8listReader.readLine(); + if (_currentFirstResponseLine != null && + _currentFirstResponseLine.startsWith('-ERR')) { + onServerResponse([_currentFirstResponseLine]); + return; + } + } + if (_currentCommand.isMultiLine) { + var lines = _uint8listReader.readLinesToCrLfDotCrLfSequence(); + if (lines != null) { + if (_currentFirstResponseLine != null) { + lines.insert(0, _currentFirstResponseLine); + } + onServerResponse(lines); + } + } else if (_currentFirstResponseLine != null) { + onServerResponse([_currentFirstResponseLine]); + } + } + + /// Upgrades the current insure connection to SSL. + /// + /// Opportunistic TLS (Transport Layer Security) refers to extensions + /// in plain text communication protocols, which offer a way to upgrade a plain text connection + /// to an encrypted (TLS or SSL) connection instead of using a separate port for encrypted communication. + Future startTls() async { + var response = await sendCommand(PopStartTlsCommand()); + if (response.isOkStatus) { + _log('STTL: upgrading socket to secure one...'); + var secureSocket = await SecureSocket.secure(_socket); + if (secureSocket != null) { + _log('STTL: now using secure connection.'); + _isSocketClosingExpected = true; + await _socket.close(); + await _socket.destroy(); + _isSocketClosingExpected = false; + connect(secureSocket); + } + } + return response; + } + + /// Logs the user in with the default `USER` and `PASS` commands. + Future login(String name, String password) async { + var response = await sendCommand(PopUserCommand(name)); + if (response.isFailedStatus) { + return response; + } + response = await sendCommand(PopPassCommand(password)); + _isLoggedIn = response.isOkStatus; + return response; + } + + /// Logs the user in with the `APOP` command. + Future loginWithApop(String name, String password) async { + var response = await sendCommand( + PopApopCommand(name, password, _serverInfo?.timestamp)); + _isLoggedIn = response.isOkStatus; + return response; + } + + /// Ends the POP session and also removes any messages that have been marked as deleted + Future quit() async { + var response = await sendCommand(PopQuitCommand(this)); + _isLoggedIn = false; + return response; + } + + /// Checks the status ie the total number of messages and their size + Future> status() { + return sendCommand(PopStatusCommand()); + } + + /// Checks the ID and size of all messages or of the message with the specified [messageId] + Future>> list([int messageId]) { + return sendCommand(PopListCommand(messageId)); + } + + /// Checks the ID and UID of all messages or of the message with the specified [messageId] + /// This command is optional and may not be supported by all servers. + Future>> uidList([int messageId]) { + return sendCommand(PopUidListCommand(messageId)); + } + + /// Downloads the message with the specified [messageId] + Future> retrieve(int messageId) { + return sendCommand(PopRetrieveCommand(messageId)); + } + + /// Downloads the first [numberOfLines] lines of the message with the [messageId] + Future> retrieveTopLines( + int messageId, int numberOfLines) { + return sendCommand(PopRetrieveCommand(messageId)); + } + + /// Marks the message with the specified [messageId] as deleted + Future delete(int messageId) { + return sendCommand(PopDeleteCommand(messageId)); + } + + /// Keeps any messages that are marked as deleted + Future reset() { + return sendCommand(PopResetCommand()); + } + + /// Keeps the connection alive + Future noop() { + return sendCommand(PopNoOpCommand()); + } + + Future sendCommand(PopCommand command) { + _currentCommand = command; + _currentFirstResponseLine = null; + _log('C: ${command.command}'); + _socket?.write(command.command + '\r\n'); + return command.completer.future; + } + + void write(String commandText) { + _log('C: $commandText'); + _socket?.write(commandText + '\r\n'); + } + + void onServerResponse(List responseTexts) { + if (_isLogEnabled) { + for (var responseText in responseTexts) { + _log('S: $responseText'); + } + } + var command = _currentCommand; + if (command == null) { + print( + 'ignoring response starting with [${responseTexts.first}] with ${responseTexts.length} lines.'); + } + if (command != null) { + var parser = command.parser; + parser ??= _standardParser; + var response = parser.parse(responseTexts); + var commandText = command.nextCommand(response); + if (commandText != null) { + write(commandText); + } else if (command.isCommandDone(response)) { + command.completer.complete(response); + //_log("Done with command ${_currentCommand.command}"); + _currentCommand = null; + } + } + } + + Future close() { + _isSocketClosingExpected = true; + return _socket?.close(); + } + + void _log(String text) { + if (_isLogEnabled) { + if (text.startsWith('C: PASS ')) { + text = 'C: PASS '; + } + print(text); + } + } +} diff --git a/lib/pop/pop_events.dart b/lib/pop/pop_events.dart new file mode 100644 index 00000000..58b3502f --- /dev/null +++ b/lib/pop/pop_events.dart @@ -0,0 +1,11 @@ +enum PopEventType { connectionLost, unknown } + +class PopEvent { + PopEventType type; + + PopEvent(this.type); +} + +class PopConnectionLostEvent extends PopEvent { + PopConnectionLostEvent() : super(PopEventType.connectionLost); +} diff --git a/lib/pop/pop_response.dart b/lib/pop/pop_response.dart new file mode 100644 index 00000000..516c2654 --- /dev/null +++ b/lib/pop/pop_response.dart @@ -0,0 +1,22 @@ +class PopResponse { + bool isOkStatus; + bool get isFailedStatus => !isOkStatus; + T result; + + PopResponse(); +} + +class PopStatus { + int numberOfMessages; + int totalSizeInBytes; +} + +class MessageListing { + int id; + String uid; + int sizeInBytes; +} + +class PopServerInfo { + String timestamp; +} diff --git a/lib/smtp/smtp_client.dart b/lib/smtp/smtp_client.dart index 87713151..629edf5f 100644 --- a/lib/smtp/smtp_client.dart +++ b/lib/smtp/smtp_client.dart @@ -9,7 +9,7 @@ import 'package:enough_mail/src/smtp/smtp_command.dart'; import 'package:enough_mail/src/smtp/commands/all_commands.dart'; import 'package:enough_mail/src/util/uint8_list_reader.dart'; -/// Keeps information about the remote IMAP server +/// Keeps information about the remote SMTP server /// /// Persist this information to improve initialization times. class SmtpServerInfo { @@ -22,16 +22,16 @@ class SmtpServerInfo { /// Low-level SMTP library for Dartlang /// -/// Compliant to Extended SMTP standard [RFC 5321]. +/// Compliant to [Extended SMTP standard](https://tools.ietf.org/html/rfc5321). class SmtpClient { - /// Information about the IMAP service + /// Information about the SMTP service SmtpServerInfo serverInfo; /// Allows to listens for events /// /// If no event bus is specified in the constructor, an aysnchronous bus is used. /// Usage: - /// ``` + /// ```dart /// eventBus.on().listen((event) { /// // All events are of type SmtpConnectionLostEvent (or subtypes of it). /// _log(event.type); diff --git a/lib/src/pop/commands/all_commands.dart b/lib/src/pop/commands/all_commands.dart new file mode 100644 index 00000000..66ed7080 --- /dev/null +++ b/lib/src/pop/commands/all_commands.dart @@ -0,0 +1,13 @@ +export 'pop_apop_command.dart'; +export 'pop_connect_command.dart'; +export 'pop_delete_command.dart'; +export 'pop_list_command.dart'; +export 'pop_noop_command.dart'; +export 'pop_pass_command.dart'; +export 'pop_quit_command.dart'; +export 'pop_reset_command.dart'; +export 'pop_retrieve_command.dart'; +export 'pop_starttls_command.dart'; +export 'pop_status_command.dart'; +export 'pop_uidl_command.dart'; +export 'pop_user_command.dart'; diff --git a/lib/src/pop/commands/pop_apop_command.dart b/lib/src/pop/commands/pop_apop_command.dart new file mode 100644 index 00000000..e523369b --- /dev/null +++ b/lib/src/pop/commands/pop_apop_command.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopApopCommand extends PopCommand { + PopApopCommand(String user, String pass, String serverTimestamp) + : super('APOP $user ${toMd5(serverTimestamp + pass)}'); + + static String toMd5(String input) { + var inputBytes = utf8.encode(input); + var digest = md5.convert(inputBytes); + return digest.toString(); + } +} diff --git a/lib/src/pop/commands/pop_connect_command.dart b/lib/src/pop/commands/pop_connect_command.dart new file mode 100644 index 00000000..8b82d7ca --- /dev/null +++ b/lib/src/pop/commands/pop_connect_command.dart @@ -0,0 +1,10 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/pop/pop_client.dart'; +import 'package:enough_mail/src/pop/parsers/pop_connection_parser.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopConnectCommand extends PopCommand { + PopConnectCommand(PopClient client) + : super('', + parser: PopConnectionParser(client)); +} diff --git a/lib/src/pop/commands/pop_delete_command.dart b/lib/src/pop/commands/pop_delete_command.dart new file mode 100644 index 00000000..0c3aacb0 --- /dev/null +++ b/lib/src/pop/commands/pop_delete_command.dart @@ -0,0 +1,5 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopDeleteCommand extends PopCommand { + PopDeleteCommand(int messageId) : super('LIST $messageId'); +} diff --git a/lib/src/pop/commands/pop_list_command.dart b/lib/src/pop/commands/pop_list_command.dart new file mode 100644 index 00000000..5608859c --- /dev/null +++ b/lib/src/pop/commands/pop_list_command.dart @@ -0,0 +1,9 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/parsers/pop_list_parser.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopListCommand extends PopCommand> { + PopListCommand([int messageId]) + : super(messageId == null ? 'LIST' : 'LIST $messageId', + parser: PopListParser(), isMultiLine: (messageId == null)); +} diff --git a/lib/src/pop/commands/pop_noop_command.dart b/lib/src/pop/commands/pop_noop_command.dart new file mode 100644 index 00000000..d95afc6f --- /dev/null +++ b/lib/src/pop/commands/pop_noop_command.dart @@ -0,0 +1,5 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopNoOpCommand extends PopCommand { + PopNoOpCommand() : super('NOOP'); +} diff --git a/lib/src/pop/commands/pop_pass_command.dart b/lib/src/pop/commands/pop_pass_command.dart new file mode 100644 index 00000000..68ebae65 --- /dev/null +++ b/lib/src/pop/commands/pop_pass_command.dart @@ -0,0 +1,5 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopPassCommand extends PopCommand { + PopPassCommand(String pass) : super('PASS $pass'); +} diff --git a/lib/src/pop/commands/pop_quit_command.dart b/lib/src/pop/commands/pop_quit_command.dart new file mode 100644 index 00000000..a37863dc --- /dev/null +++ b/lib/src/pop/commands/pop_quit_command.dart @@ -0,0 +1,14 @@ +import 'package:enough_mail/pop/pop_client.dart'; +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopQuitCommand extends PopCommand { + final PopClient _client; + PopQuitCommand(this._client) : super('QUIT'); + + @override + String nextCommand(PopResponse response) { + _client.close(); + return null; + } +} diff --git a/lib/src/pop/commands/pop_reset_command.dart b/lib/src/pop/commands/pop_reset_command.dart new file mode 100644 index 00000000..bb505d54 --- /dev/null +++ b/lib/src/pop/commands/pop_reset_command.dart @@ -0,0 +1,5 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopResetCommand extends PopCommand { + PopResetCommand() : super('RSET'); +} diff --git a/lib/src/pop/commands/pop_retrieve_command.dart b/lib/src/pop/commands/pop_retrieve_command.dart new file mode 100644 index 00000000..421d765e --- /dev/null +++ b/lib/src/pop/commands/pop_retrieve_command.dart @@ -0,0 +1,9 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/src/pop/parsers/all_parsers.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopRetrieveCommand extends PopCommand { + PopRetrieveCommand(int messageId) + : super('RETR $messageId', + parser: PopRetrieveParser(), isMultiLine: true); +} diff --git a/lib/src/pop/commands/pop_starttls_command.dart b/lib/src/pop/commands/pop_starttls_command.dart new file mode 100644 index 00000000..7f0749ce --- /dev/null +++ b/lib/src/pop/commands/pop_starttls_command.dart @@ -0,0 +1,6 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +/// Compare https://tools.ietf.org/html/rfc2595 +class PopStartTlsCommand extends PopCommand { + PopStartTlsCommand() : super('STLS'); +} diff --git a/lib/src/pop/commands/pop_status_command.dart b/lib/src/pop/commands/pop_status_command.dart new file mode 100644 index 00000000..b0aee2cc --- /dev/null +++ b/lib/src/pop/commands/pop_status_command.dart @@ -0,0 +1,7 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/parsers/pop_status_parser.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopStatusCommand extends PopCommand { + PopStatusCommand() : super('STAT', parser: PopStatusParser()); +} diff --git a/lib/src/pop/commands/pop_top_command.dart b/lib/src/pop/commands/pop_top_command.dart new file mode 100644 index 00000000..06e83ae9 --- /dev/null +++ b/lib/src/pop/commands/pop_top_command.dart @@ -0,0 +1,9 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/src/pop/parsers/all_parsers.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopTopCommand extends PopCommand { + PopTopCommand(int messageId, int lines) + : super('TOP $messageId $lines', + parser: PopRetrieveParser(), isMultiLine: true); +} diff --git a/lib/src/pop/commands/pop_uidl_command.dart b/lib/src/pop/commands/pop_uidl_command.dart new file mode 100644 index 00000000..c670f526 --- /dev/null +++ b/lib/src/pop/commands/pop_uidl_command.dart @@ -0,0 +1,9 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/parsers/all_parsers.dart'; +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopUidListCommand extends PopCommand> { + PopUidListCommand([int messageId]) + : super(messageId == null ? 'LIST' : 'LIST $messageId', + parser: PopUidListParser(), isMultiLine: (messageId == null)); +} diff --git a/lib/src/pop/commands/pop_user_command.dart b/lib/src/pop/commands/pop_user_command.dart new file mode 100644 index 00000000..34a2f6c6 --- /dev/null +++ b/lib/src/pop/commands/pop_user_command.dart @@ -0,0 +1,5 @@ +import 'package:enough_mail/src/pop/pop_command.dart'; + +class PopUserCommand extends PopCommand { + PopUserCommand(String user) : super('USER $user'); +} diff --git a/lib/src/pop/parsers/all_parsers.dart b/lib/src/pop/parsers/all_parsers.dart new file mode 100644 index 00000000..0f463031 --- /dev/null +++ b/lib/src/pop/parsers/all_parsers.dart @@ -0,0 +1,6 @@ +export 'pop_connection_parser.dart'; +export 'pop_list_parser.dart'; +export 'pop_retrieve_parser.dart'; +export 'pop_standard_parser.dart'; +export 'pop_status_parser.dart'; +export 'pop_uidl_parser.dart'; diff --git a/lib/src/pop/parsers/pop_connection_parser.dart b/lib/src/pop/parsers/pop_connection_parser.dart new file mode 100644 index 00000000..e3991cd1 --- /dev/null +++ b/lib/src/pop/parsers/pop_connection_parser.dart @@ -0,0 +1,23 @@ +import 'package:enough_mail/pop/pop_client.dart'; +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +/// Parses responses to STATUS command +class PopConnectionParser extends PopResponseParser { + final PopClient _client; + + PopConnectionParser(this._client); + + @override + PopResponse parse(List responseLines) { + var response = PopResponse(); + parseOkStatus(responseLines, response); + if (response.isOkStatus) { + var responseLine = responseLines.first; + var chunks = responseLine.split(' '); + response.result = PopServerInfo()..timestamp = chunks.last; + _client.serverInfo = response.result; + } + return response; + } +} diff --git a/lib/src/pop/parsers/pop_list_parser.dart b/lib/src/pop/parsers/pop_list_parser.dart new file mode 100644 index 00000000..ed370b6c --- /dev/null +++ b/lib/src/pop/parsers/pop_list_parser.dart @@ -0,0 +1,33 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +class PopListParser extends PopResponseParser> { + @override + PopResponse> parse(List responseLines) { + var response = PopResponse>(); + parseOkStatus(responseLines, response); + if (response.isOkStatus) { + var result = []; + response.result = result; + for (var line in responseLines) { + if (line == '+OK') { + continue; + } + var parts = line.split(' '); + var listing = MessageListing(); + if (parts.length == 2) { + listing.id = int.tryParse(parts[0]); + listing.sizeInBytes = int.tryParse(parts[1]); + } else if (parts.length == 3) { + // eg '+OK 123 123231' + listing.id = int.tryParse(parts[1]); + listing.sizeInBytes = int.tryParse(parts[2]); + } else { + print('Unexpected LIST response line [$line]'); + } + result.add(listing); + } + } + return response; + } +} diff --git a/lib/src/pop/parsers/pop_retrieve_parser.dart b/lib/src/pop/parsers/pop_retrieve_parser.dart new file mode 100644 index 00000000..ce8077e5 --- /dev/null +++ b/lib/src/pop/parsers/pop_retrieve_parser.dart @@ -0,0 +1,27 @@ +import 'package:enough_mail/enough_mail.dart'; +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +class PopRetrieveParser extends PopResponseParser { + @override + PopResponse parse(List responseLines) { + var response = PopResponse(); + parseOkStatus(responseLines, response); + if (response.isOkStatus) { + var message = MimeMessage(); + //lines that start with a dot need to remove the dot first: + var buffer = StringBuffer(); + for (var i = 1; i < responseLines.length; i++) { + var line = responseLines[i]; + if (line.startsWith('.') && line.length > 1) { + line = line.substring(1); + } + buffer..write(line)..write('\r\n'); + } + message.bodyRaw = buffer.toString(); + message.parse(); + response.result = message; + } + return response; + } +} diff --git a/lib/src/pop/parsers/pop_standard_parser.dart b/lib/src/pop/parsers/pop_standard_parser.dart new file mode 100644 index 00000000..66b80456 --- /dev/null +++ b/lib/src/pop/parsers/pop_standard_parser.dart @@ -0,0 +1,12 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +class PopStandardParser extends PopResponseParser { + @override + PopResponse parse(List responseLines) { + var response = PopResponse(); + response.result = responseLines.isEmpty ? null : responseLines.first; + parseOkStatus(responseLines, response); + return response; + } +} diff --git a/lib/src/pop/parsers/pop_status_parser.dart b/lib/src/pop/parsers/pop_status_parser.dart new file mode 100644 index 00000000..ec03a18b --- /dev/null +++ b/lib/src/pop/parsers/pop_status_parser.dart @@ -0,0 +1,23 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +/// Parses responses to STATUS command +class PopStatusParser extends PopResponseParser { + @override + PopResponse parse(List responseLines) { + var response = PopResponse(); + parseOkStatus(responseLines, response); + if (response.isOkStatus) { + var responseLine = responseLines.first; + if (responseLine.length > '+OK '.length) { + var parts = responseLine.substring('+OK '.length).split(' '); + var status = PopStatus()..numberOfMessages = int.tryParse(parts[0]); + if (parts.length > 1) { + status.totalSizeInBytes = int.tryParse(parts[1]); + } + response.result = status; + } + } + return response; + } +} diff --git a/lib/src/pop/parsers/pop_uidl_parser.dart b/lib/src/pop/parsers/pop_uidl_parser.dart new file mode 100644 index 00000000..f9000a6d --- /dev/null +++ b/lib/src/pop/parsers/pop_uidl_parser.dart @@ -0,0 +1,33 @@ +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +class PopUidListParser extends PopResponseParser> { + @override + PopResponse> parse(List responseLines) { + var response = PopResponse>(); + parseOkStatus(responseLines, response); + if (response.isOkStatus) { + var result = []; + response.result = result; + for (var line in responseLines) { + if (line.isEmpty || line == '+OK') { + continue; + } + var parts = line.split(' '); + var listing = MessageListing(); + if (parts.length == 2) { + listing.id = int.tryParse(parts[0]); + listing.uid = parts[1]; + } else if (parts.length == 3) { + // eg '+OK 123 123231' + listing.id = int.tryParse(parts[1]); + listing.uid = parts[2]; + } else { + print('Unexpected UIDL response line [$line]'); + } + result.add(listing); + } + } + return response; + } +} diff --git a/lib/src/pop/pop_command.dart b/lib/src/pop/pop_command.dart new file mode 100644 index 00000000..7bffc0fa --- /dev/null +++ b/lib/src/pop/pop_command.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'package:enough_mail/pop/pop_response.dart'; +import 'package:enough_mail/src/pop/pop_response_parser.dart'; + +class PopCommand { + final String _command; + PopResponseParser parser; + bool isMultiLine; + + String get command => getCommand(); + + final Completer> completer = Completer>(); + + PopCommand(this._command, + {PopResponseParser parser, bool isMultiLine = false}) { + this.parser = parser; + this.isMultiLine = isMultiLine; + } + + String getCommand() { + return _command; + } + + String nextCommand(PopResponse response) { + return null; + } + + bool isCommandDone(PopResponse response) { + return true; + } +} diff --git a/lib/src/pop/pop_response_parser.dart b/lib/src/pop/pop_response_parser.dart new file mode 100644 index 00000000..0f231abb --- /dev/null +++ b/lib/src/pop/pop_response_parser.dart @@ -0,0 +1,12 @@ +import 'package:enough_mail/enough_mail.dart'; + +/// Parses POP responses +abstract class PopResponseParser { + /// Parses the OK status of the response + void parseOkStatus(List responseLines, PopResponse response) { + response.isOkStatus = + (responseLines.isNotEmpty && responseLines.first.startsWith('+OK')); + } + + PopResponse parse(List responseLines); +} diff --git a/lib/src/util/ascii_runes.dart b/lib/src/util/ascii_runes.dart index 0942eed6..a02a742c 100644 --- a/lib/src/util/ascii_runes.dart +++ b/lib/src/util/ascii_runes.dart @@ -3,6 +3,7 @@ class AsciiRunes { static const int runeSpace = 32; static const int runeDoubleQuote = 34; static const int runeSingleQuote = 39; + static const int runeDot = 46; static const int rune0 = 48; static const int rune9 = 57; static const int runeComma = 44; diff --git a/lib/src/util/uint8_list_reader.dart b/lib/src/util/uint8_list_reader.dart index 86ed4ab5..b7bc3c6f 100644 --- a/lib/src/util/uint8_list_reader.dart +++ b/lib/src/util/uint8_list_reader.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; +import 'package:enough_mail/src/util/ascii_runes.dart'; + /// Combines several Uin8Lists to read from them sequentially class Uint8ListReader { Uint8List _data = Uint8List(0); @@ -70,6 +72,36 @@ class Uint8ListReader { return text.split('\r\n'); } + int findLastCrLfDotCrLfSequence() { + var data = _data; + for (var charIndex = data.length; --charIndex > 4;) { + if (data[charIndex] == 10 && + data[charIndex - 1] == 13 && + data[charIndex - 2] == AsciiRunes.runeDot && + data[charIndex - 3] == 10 && + data[charIndex - 4] == 13) { + // ok found CRLF.CRLF sequence: + return charIndex; + } + } + return null; + } + + List readLinesToCrLfDotCrLfSequence() { + var pos = findLastCrLfDotCrLfSequence(); + if (pos == null) { + return null; + } + String text; + text = String.fromCharCodes(_data, 0, pos - 4); + if (pos == _data.length - 1) { + _data = Uint8List(0); + } else { + _data = _data.sublist(pos + 1); + } + return text.split('\r\n'); + } + Uint8List readBytes(int length) { if (!isAvailable(length)) { return null; diff --git a/pubspec.yaml b/pubspec.yaml index 9efcf9a1..52710a94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: enough_mail -description: IMAP and SMTP clients in pure Dart. Strives to be compliant with IMAP4 rev1, IMAP IDLE, IMAP METADATA Extension and SMTP. -version: 0.0.22 +description: IMAP, POP3 and SMTP clients in pure Dart. Provides low level support for these mail protocols. +version: 0.0.23 homepage: https://github.com/Enough-Software/enough_mail environment: @@ -9,6 +9,7 @@ environment: dependencies: event_bus: ^1.1.1 intl: ^0.16.1 + crypto: ^2.1.5 dev_dependencies: pedantic: ^1.9.0