Несмотря на активное использование мессенджеров, электронная почта все еще занимает весомую долю в коммуникации, особенно в рабочей среде, из-за этого не исключена необходимость в отправке почтовых сообщений прямо с iOS приложения.
В данной статье мы ознакомимся с протоколами, по которым работает почта. Сфокусируемся на реализации протокола для отправки почтовых сообщений (SMTP) на низком уровне (BSD сокеты). И, используя собственный сетевой слой для работы с почтой, реализуем iOS клиент для отправки почтовых сообщений через любые почтовые провайдеры (ex: gmail, yandex, mail).
Оглавление
Эту статью можно воспринимать обособленно, но в ней я не буду детально останавливаться на слоях сетевого стека, TCP/UDP сокетах, интерфейсе BSD сокетов (в основе которых будет итоговая реализация). Всех этих аспектов я уже касался в статье про самописный HTTP сервер на сокетах. Эту статью можно воспринимать как некого рода продолжение изучения реализации "с нуля" сетевых протоколов Application уровня на swift. В прошлой части сделали HTTP, тут будем работать с SMTP, о котором и будет дальнейшее повествование.
По каким протоколам работает электронная почта?
Работая с мобильными клиент-серверными приложениями в качестве протокола application слоя мы чаще всего работаем с HTTPS. Этот протокол позволяет как получать данные с сервера, так и отправлять данные ему же. С почтой все немного сложнее.
На сегодняшний день существует три основных протокола по работе с почтой: SMTP, POP3 и IMAP. POP3 и IMAP - это протоколы только для получения почтовых сообщений с сервера. SMTP же - протокол для отправки почтовых сообщений.
POP3. Рассматривая протоколы получения, POP3 появился первым, но сегодня считается более устаревшим и менее функциональным, из-за этого он теряет свои позиции и практически нигде не используется. POP3 проектировался для перекачки сообщений с сервера на клиент. Тогда еще у каждого человека не было десятка девайсов, между которыми требовалась синхронизация, из-за этого протокол лишен множества необходимых на сегодняшний день функций (например, возможность пометить сообщение как прочитанное).
IMAP. Современная замена POP3, спроектированная под хранение почты на сервере и получение сообщений с множества устройств, с различными возможностями настройки (работа с папками, флаги сообщений, ...).
SMTP. Протокол только для отправки почтовых сообщений (от клиента к серверу, либо между серверами). Тут нет разделения, как с протоколами получения. Используется широко по сегодняшний день.

Для реализации полноценного почтового клиента требуется связка протоколов IMAP/SMTP. Мы сфокусируемся только на отправке, соответственно нам будет интересен только SMTP.
Перед тем как мы уйдем в реализацию, стоит подсветить альтернативы решаемой задаче:
MFMailComposeViewController - готовое решение, позволяющее отобразить пользователю UI родной iOS почты, через который пользователь может отправить сообщение. Простота использования - очевидный плюс, нулевая гибкость - минус.
Готовая библиотека, реализующая SMTP, например SwiftSMTP. Чуть сложнее для использования (по сравнению с первым вариантом), но полная гибкость для реализации. Из минусов - необходимость в запросе логина и пароля приложения для доступа к почтовому серверу.
Своя реализация SMTP. Плюсы и минусы аналогичны пункту 2, за исключением того, что реализацию необходимо написать самому. Подойдет, если по каким-то причинам нельзя использовать third-party библиотеки (или если вы хотите разобраться глубже в том как это все работает).
Для классических сценариев по типу "нажмите сюда, чтоб написать на почту поддержки" достаточно варианта 1. Для каких-то более продвинутых интеграций с почтой потребуется выбрать между 2 и 3 вариантом. Ниже мы пойдем путем своей реализации.
Разбираем протокол SMTP через терминал
Перед реализацией нужно понять, по каким правилам вообще строится коммуникация по этому протоколу. Буду иногда ссылаться на HTTP, так как с ним большее количество разработчиков знакомы. Полное описание SMTP протокола можно почитать в RFC 5321.
Взаимодействие, как и по HTTP, осуществляется посредством команд. Фундаментальное отличие от HTTP заключается в том, что в SMTP для отправки одного письма необходимо выполнять ряд команд в определенной последовательности (транзакционная модель). А в HTTP каждый запрос независим и обрабатывается отдельно.
Давайте руками пройдем всю последовательность команд для отправки сообщения. Пока у нас нет написанного кода на swift - будем тестировать SMTP команды в терминале:
С помощью команды openssl s_client -connect smtp.gmail.com:465 -crlf -quiet устанавливаем соединение с SMTP сервером gmail. Стоит сразу сказать, что мы фактически пользуемся SMTP поверх TLS. SMTP без TLS шифрования современные серверы уже практически не поддерживают (аналогия с HTTP и HTTPS).

Запрос и ответ текстом
openssl s_client -connect smtp.gmail.com:465 -crlf -quiet Connecting to 2607:f8b0:4002:c0f::6d depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R4 verify return:1 depth=1 C=US, O=Google Trust Services, CN=WE2 verify return:1 depth=0 CN=smtp.gmail.com verify return:1 220 smtp.gmail.com ESMTP 956f58d0204a3-660d64ba680sm4086602d50.21 - gsmtp
Тут мы можем сразу увидеть формат ответа сервера (выделено зеленым). Он достаточно простой, имеет универсальный формат:
XXX[' ' or '-']TEXT
Где XXX - это код ответа (аналогично HTTP status code 200, 404 и так далее), где есть определенный заложенный набор значений. Тут мы видим, например, 220, который означает Service ready. И все коды, начинающиеся с 2xx, считаются успешными ответами. Список SMTP response codes.
После кода идет либо пробел, либо тире. Пробел означает, что это последняя строчка респонса, а тире - что ответ многострочный, и далее будут еще строки (пока не дойдем до строки с пробелом после кода). После этого уже идет просто человеко-читаемый произвольный статус. Это весь формат ответа.
Далее мы обязаны представиться серверу, используя команду EHLO {name}. Name по RFC должен соответствовать доменному имени, но на практике туда можно отправить произвольное имя, обычно это не валидируется (но где-то может). Каждая команда должна заканчиваться CRLF (\r\n), на скринах с терминала этого видно не будет, но в коде нужно не забывать.
Вместо
EHLOможно использовать альтернативуHELO, но RFC требует использование именно первой. SMTP сервер от Mail, например, не обработает последовательность команд, если начать сHELO. Но некоторые серверы работают и сHELO, например gmail.

Запрос и ответ текстом
EHLO ios-smtp-client 250-smtp.gmail.com at your service 250-SIZE 35882577 250-8BITMIME 250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH 250-ENHANCEDSTATUSCODES 250-PIPELINING 250-CHUNKING 250 SMTPUTF8
На примере этой команды как раз можно увидеть многострочный ответ сервера. Как и писал до этого, после кода идет -, на последней строке из ответа после кода пробел. Знак того, что это последняя строка ответа.
Вместе с EHLO сервер прислал некоторую информацию о себе (SMTP extensions). Например, SIZE - максимальный размер письма, которое примет сервер. AUTH - поддерживаемые способы аутентификации. PIPELINING - возможность отправить несколько команд сразу (этого еще коснемся позже). Со списком можно ознакомиться тут.
Далее инициируем процесс авторизации с помощью команды AUTH LOGIN. Способов авторизации тоже достаточно, почитать можно тут. И сервер нам прислал выше список поддерживаемых. Мы воспользуемся самым ходовым, требующим логин от почты и пароль приложения. Если настраивали нативную почту в macOS, или какой-то кастомный почтовый клиент - сталкивались с этим.
Для gmail пароль приложения можно создать тут
Для Яндекса тут (обязательно выбрать тип приложения "почта", там как раз перечислены нужные протоколы)
Для mail тут (при создании достаточно выбрать "только отправка писем", так как мы используем только SMTP)

Запрос и ответ текстом
AUTH LOGIN 334 VXNlcm5hbWU6 aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQ== 334 UGFzc3dvcmQ6 aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQ== 235 2.7.0 Accepted
После команды авторизации видим код 334 (код, сигнализирующий о том, что сервер принял команду и ожидает данные). Дальше идет каша из символов, но на самом деле это просто закодированное в Base64 сообщение. В первом ответе (VXNlcm5hbWU6) оно декодируется как Username:, во втором (UGFzc3dvcmQ6) Password:. Отправлять серверу логин и пароль нужно тоже в base64. Если что это не для безопасности. SMTP - 7-битный ASCII протокол, и Base64 кодирует данные в совместимый с ним формат. В конце мы видим 235 2.7.0 Accepted. Авторизация успешна.
Далее приступаем к отправке письма. Первым делом нужно сказать серверу от кого письмо с помощью команды MAIL FROM:<MAIL> (угловые скобки обязательны по RFC, без них тот же gmail дропает ошибку, но некоторые серверы не требуют). Значение почты должно быть то, с которого вы логинились. От чужой почты отправить сообщение сервер не даст (к сожалению).

Запрос и ответ текстом
MAIL FROM:<youremail@gmail.com> 250 2.1.0 OK 956f58d0204a3-660d64ba680sm4086602d50.21 - gsmtp
Далее вводим получателя, с помощью команды RCPT TO:<MAIL>. Можно вводить сколько угодно подряд раз, если получателей несколько.

Запрос и ответ текстом
RCPT TO:<recipientemail@yandex.ru> 250 2.1.5 OK 956f58d0204a3-660d64ba680sm4086602d50.21 - gsmtp
Финальный шаг, осталось отправить само сообщение. Для этого используется команда DATA, после нее можем вводить произвольное кол-во строк. В качестве триггера завершения ввода используется символ точки .

Запрос и ответ текстом
DATA 354 Go ahead 956f58d0204a3-660d64ba680sm4086602d50.21 - gsmtp HELLO WORLD . 250 2.0.0 OK 1780590458 956f58d0204a3-660d64ba680sm4086602d50.21 - gsmtp
По результату видим 250. Поздравляю, мы отправили письмо. SMTP выглядит архаично, сравнивая с HTTP, где нам бы понадобился для отправки всего один запрос, в который мы укомплектовали бы все данные (и многие почтовые сервисы предоставляют такой API, например google). Это удобно, если вам нужен клиент для одного почтового сервиса. Но все равно под капотом это все сводится к SMTP, так как это уже устоявшийся стандарт работы почтовых серверов. SMTP тоже не стоит на месте, выше я упоминал про PIPELINING, который как раз и позволяет укомплектовывать несколько команд в один запрос.

Пишем iOS реализацию по работе с SMTP
Перейдем непосредственно к реализации. SMTP, как и HTTP, работает поверх TCP. Соответственно для реализации нам потребуется класс для работы с TCP сокетами на уровне клиента. Реализация будет основана на BSD сокетах. Основополагающими винтиками в реализации будут 2 объекта:
TCPConnection: инкапсулирует логику по работе с TCP сокетом. Создание соединения, чтение данных из сокета, запись в сокет и взаимодействие с TLS соединением (которое будет в виде отдельного объекта).
TLSConnection: основывается на Security framework'е от Apple и их готовых функциях для криптографии. Этот класс нужен, чтоб зашифровать данные, отправляемые через TCPConnection (и дешифровывать то что приходит из него). Мы будем отправлять запросы на реальные SMTP серверы, для общения с ними мы обязаны использовать шифрование. Класс оборачивает чтение из сокета шифрованием через SSLContext, достаточно хорошо ложится на решение (правда считается deprecated, apple пушат использовать их высокоуровневый Network framework, в который уже заложено шифрование).
Благодаря этим двум классам мы умеем безопасно получать и передавать данные по TCP сокету. Чего в целом достаточно для реализации любого Application протокола, работающего поверх TCP (в том числе HTTP).
Теперь приступим к реализации SMTP слоя. Начнем с моделей. Для начала нам нужна сущность, в которой будут храниться креды для доступа к почтовому сервису:
import Foundation struct SMTPCredentials { // 1 let host: String let proto: Proto let username: String let loadPassword: () throws -> Data var port: UInt16 { proto.port } init( host: String, proto: Proto = .smtps, username: String, loadPassword: @escaping () throws -> Data ) { self.host = host self.proto = proto self.username = username self.loadPassword = loadPassword } } extension SMTPCredentials { // 2 enum Proto { case smtps case starttls var port: UInt16 { switch self { case .smtps: 465 case .starttls: 587 } } } }
Для доступа нам нужны: сервер, к которому будем подключаться (хост), протокол подключения, имя пользователя для данного сервиса и пароль. Пароль в виде замыкания, через которое он будет доставаться из keychain, чтоб минимизировать время хранения пароля в памяти.
Протокол. Выше я уже озвучивал, что фактически мы будем использовать не SMTP, а SMTPS (c TLS шифрованием). Но для использования нам также доступен SMTP + STARTTLS, и найдутся серверы, которые используют только его. Чем они отличаются? SMTPS рассчитан на то, что клиент сразу будет осуществлять TLS handshake и все дальнейшее общение производить в зашифрованном виде (прям как HTTPS). STARTTLS в свою очередь не ожидает TLS handshake сразу при подключении. То есть начало общения клиента и сервера (первое HELO/EHLO) не будут зашифрованы. Подключение закрывается TLS шифрованием только после специальной команды
STARTTLS. Нам важно знать способ подключения, так как SMTPS работает по 465 порту, а STARTTLS по 587. Разница будет более понятна, когда будем реализовывать подключение. Все крупные почтовые сервисы сегодня поддерживают оба протокола.
Далее реализуем сущности для запроса к серверу и ответа от него. Начнем с запроса:
import Foundation enum SMTPRequest { case helo case ehlo case starttls case auth case authUsername(String) case authPassword(Data) case mailFrom(String) case rcptTo(String) case data case mailMessage(String) case quit } extension SMTPRequest { var requestData: Data { get throws { var data: Data? switch self { case .helo: data = "HELO \(Constants.clientName)\r\n".data(using: .utf8) case .ehlo: data = "EHLO \(Constants.clientName)\r\n".data(using: .utf8) case .starttls: data = "STARTTLS\r\n".data(using: .utf8) case .auth: data = "AUTH LOGIN\r\n".data(using: .utf8) case let .authUsername(username): guard let base64 = username.data(using: .utf8)?.base64EncodedString() else { data = nil break } data = "\(base64)\r\n".data(using: .utf8) case let .authPassword(password): let base64 = password.base64EncodedString() data = "\(base64)\r\n".data(using: .utf8) case let .mailFrom(mail): data = "MAIL FROM:<\(mail)>\r\n".data(using: .utf8) case let .rcptTo(mail): data = "RCPT TO:<\(mail)>\r\n".data(using: .utf8) case .data: data = "DATA\r\n".data(using: .utf8) case let .mailMessage(message): data = message.data(using: .utf8) case .quit: data = "QUIT\r\n".data(using: .utf8) } guard let data else { throw Error.invalidData("Error: invalid data for command \(self)") } return data } } } private extension SMTPRequest { enum Constants { static let clientName = "swift-smtp.client" } enum Error: Swift.Error { case invalidData(String) } }
Тут в целом просто в виде закрытого множества оформлены все команды, которые мы смотрели в терминале выше. Из важного: каждая команда заканчивается CRLF последовательностью (\r\n). Благодаря ей сервер понимает, что мы закончили команду. Также напоминаю, что username и пароль мы должны передавать в base64. Также отмечу, что это не все доступные команды (только необходимые нам).
Теперь сущность для ответа от сервера:
import Foundation struct SMTPResponse { // 1 let code: Code let message: String var response: String { "\(code.rawValue) \(message)" } // 2 var challenge: ChallengeType? { guard code == .containingChallenge, let base64Data = Data(base64Encoded: message), let decodedString = String(data: base64Data, encoding: .utf8), let challenge = ChallengeType(rawValue: decodedString) else { return nil } return challenge } } extension SMTPResponse { // 3 enum Code: Int { case serviceReady = 220 case connectionClosing = 221 case authSucceeded = 235 case commandOK = 250 case willForward = 251 case containingChallenge = 334 case startMailInput = 354 } // 2 enum ChallengeType: String { case username = "Username:" case password = "Password:" } }
Структура ответа, как уже разбирали выше, состоит из кода и сообщения.
Если код ответа 334, то рядом с ним будет закодированное в base64 сообщение. В нашем случае это будут запросы на логин и пароль. Расширение для удобного декодирования сообщений подобного типа.
Это не полный список кодов. Только то, что будет необходимо нам.
Из сервисных моделей нам еще понадобится сущность для хранения самого письма:
import Foundation struct SMTPMail { let from: String let to: String let cc: [String] let subject: String let body: String init( from: String, to: String, cc: [String] = [], subject: String, body: String ) { self.from = from self.to = to self.cc = cc self.subject = subject self.body = body } var message: String { let cc = cc.isEmpty ? "" : "\r\nCc: \(cc.joined(separator: ","))" let subjectBase64 = subject.data(using: .utf8)?.base64EncodedString() ?? "" let date = Date() let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z" let formattedDate = formatter.string(from: date) return """ From: \(from) To: \(to)\(cc) Subject: =?utf-8?B?\(subjectBase64)?= Date: \(formattedDate) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Mailer: SMTPSwift Client 1.0 Message-Id: <\(UUID().uuidString)> \(body)\r\n.\r\n """ } }
Когда мы отправляли сообщение через терминал, мы ввели туда произвольное "Hello World". Но фактически у сообщения тоже есть своя структура: От кого и к кому, заголовок, дата, тип контента, id. Это необходимо чтоб другие почтовые клиенты корректно парсили письма. Произвольные отправления, по типу которых мы отправляли в терминале, могут спокойно отлетать в спам.
Все сущности реализованы, начнем писать сам клиент:
import Foundation actor SMTPClient { private let credentials: SMTPCredentials private let connection: TCPConnection init(credentials: SMTPCredentials) { self.credentials = credentials self.connection = TCPConnection( host: credentials.host, port: credentials.port ) } func send(email: SMTPMail) throws { } } private extension SMTPClient { nonisolated enum Constants { static let CRLF = "\r\n" } enum Error: Swift.Error { case parseError(String?) case requestError(String) case serverNotReady(String) case authError(String) case sendEmailError(String) } }
Интерфейс для использования будет максимально простым. Инициализируем с SMTPCredentials и дергаем единственный метод для отправки письма, передавая туда SMTPMail. Также сразу объявим константы и ошибки, которые будут использоваться в дальнейшем.
Далее будем понемногу обогащать метод отправки, начнем с подключения:
func send(email: SMTPMail) throws { // 1 defer { connection.finish() } // 2 try connect() } private func connect() throws { // 3 try connection.connect() // 4 if credentials.proto == .smtps { try connection.tlsHandshake() } }
По завершению метода завершаем подключение в любом случае.
Выделяем логику подключения и аутентификации в отдельный метод.
Инициируем подключение к хосту через метод у
TCPConnection.Если мы подключаемся по SMTPS к 465 порту, то должны сразу осуществить TLS handshake, так как вся коммуникация должна быть зашифрована. Отдельное ветвление для STARTTLS будет далее.
Теперь нужно научиться читать и парсить то, что нам пришлет сервер:
private func connect() throws { try connection.connect() if credentials.proto == .smtps { try connection.tlsHandshake() } // 1 let statusData = try connection.read(while: endMessageCondition) // 2 let status = try parseResponse(statusData) // 3 guard status.code == .serviceReady else { throw Error.serverNotReady(status.response) } print(status.response) } // 2 private func parseResponse(_ data: Data) throws -> SMTPResponse { guard let response = String(data: data, encoding: .utf8) else { throw Error.parseError(nil) } guard response.count > 4 else { throw Error.parseError(response) } guard let code = Int(String(response.prefix(3))), let smtpCode = SMTPResponse.Code(rawValue: code) else { throw Error.parseError(response) } let message = String(response.suffix(response.count - 4)).replacingOccurrences(of: Constants.CRLF, with: "") return SMTPResponse(code: smtpCode, message: message) } // 1 private func endMessageCondition(data: Data) -> Bool { guard let string = String(data: data, encoding: .utf8), let lastRow = string.split(separator: Constants.CRLF).last, lastRow.count > 3 else { return false } return lastRow[lastRow.index(lastRow.startIndex, offsetBy: 3)] == " " }
Тут мы получаем из
TCPConnectionполный ответ с сервера. Обязательно передаем функцию, благодаря которойTCPConnectionсможет идентифицировать конец данных (так как TCP - это потоковый протокол и данные могут приходить разрозненно). Концом в нашем случае является строка, заканчивающаяся на CRLF и обязательно имеющая пробел после статус кода (после первых трех символов).Преобразуем полученные с предыдущего шага данные в наш SMTPResponse. Логика тоже достаточно простая. Первые три символа отводятся на код, все остальное - это message.
Удостоверяемся, что при подключении сервер прислал нам 220 код, говорящий нам о том, что он готов к работе.
Читать мы научились, теперь нам нужен метод для отправки команды:
private func connect() throws { try connection.connect() if credentials.proto == .smtps { try connection.tlsHandshake() } let statusData = try connection.read(while: endMessageCondition) let status = try parseResponse(statusData) guard status.code == .serviceReady else { throw Error.serverNotReady(status.response) } print(status.response) // 1 let helloResponse = try sendRequest(.ehlo) // 2 guard helloResponse.code == .commandOK else { throw Error.serverNotReady(helloResponse.response) } } // 1 @discardableResult private func sendRequest(_ request: SMTPRequest) throws -> SMTPResponse { try connection.write(data: request.requestData) let responseData = try connection.read(while: endMessageCondition) return try parseResponse(responseData) }
Выделяем отдельный метод для отправки запроса. Он внутри себя отправляет команду в
TCPConnection, а далее читает ответ сервера и парсит его вSMTPResponse(с помощью методов, которые мы реализовали на предыдущем шаге).Проверяем что в ответе мы получили 250 код, означающий успех (далее часто будем проверять ответ на этот код).
Если мы подключаемся к серверу через STARTTLS по 587 порту, то на текущий момент мы все еще общаемся в незашифрованном виде (кто угодно может прослушать общение с сервером), ведь tls handshake мы совершали выше только для SMTPS подключения. Настало время сделать это и для STARTTLS, так как это совершается как раз после EHLO.
private func connect() throws { try connection.connect() if credentials.proto == .smtps { try connection.tlsHandshake() } let statusData = try connection.read(while: endMessageCondition) let status = try parseResponse(statusData) guard status.code == .serviceReady else { throw Error.serverNotReady(status.response) } print(status.response) let helloResponse = try sendRequest(.ehlo) guard helloResponse.code == .commandOK else { throw Error.serverNotReady(helloResponse.response) } // 1 if credentials.proto == .starttls { let starttlsResponse = try sendRequest(.starttls) guard starttlsResponse.code == .serviceReady else { throw Error.serverNotReady(starttlsResponse.response) } try connection.tlsHandshake() let hello2Response = try sendRequest(.ehlo) guard hello2Response.code == .commandOK else { throw Error.serverNotReady(hello2Response.response) } } }
Только для 587 порта отправляем команду STARTTLS, после этого делаем tls handshake, и после еще раз отправляем EHLO (уже в зашифрованном виде) и удостоверяемся в успешном ответе от сервера. Зашифрованное соединение установлено.
На этом вся разница между SMTPS и STARTTLS. Далее флоу будет идентичный. Осуществляем авторизацию (по зашифрованному каналу для обоих протоколов).
private func connect() throws { // ... // 1 let usernameResponse = try sendRequest(.auth) guard usernameResponse.challenge == .username else { throw Error.authError(usernameResponse.response) } let passwordResponse = try sendRequest(.authUsername(credentials.username)) // 2 guard passwordResponse.challenge == .password else { throw Error.authError(passwordResponse.response) } let passwordData = try credentials.loadPassword() let authResponse = try sendRequest(.authPassword(passwordData)) // 3 guard authResponse.code == .authSucceeded else { throw Error.authError(authResponse.response) } print(authResponse.response) }
Отправляем команду
AUTH LOGIN, удостоверяемся что сервер после этого запросил у нас username и отправляем ему его в ответ.После отправленного username сервер должен запросить пароль. Проверяем это и отправляем пароль.
Удостоверяемся что сервер подтвердил аутентификацию.
На этом наш метод connect реализован. Мы соединились с сервером, установили зашифрованное подключение и прошли аутентификацию. Нам осталось только отправить сообщение:
func send(email: SMTPMail) throws { defer { connection.finish() } try connect() // 1 let fromResponse = try sendRequest(.mailFrom(email.from)) guard fromResponse.code == .commandOK else { throw Error.sendEmailError(fromResponse.response) } print(fromResponse.response) // 2 for mail in [email.to] + email.cc { do { let toResponse = try sendRequest(.rcptTo(mail)) print(toResponse.response) } catch { print("Failed setting \(mail) as recipient: \(error)") } } // 3 let dataResponse = try sendRequest(.data) guard dataResponse.code == .startMailInput else { throw Error.sendEmailError(dataResponse.response) } print(dataResponse.response) // 4 let dataEndResponse = try sendRequest(.mailMessage(email.message)) guard dataEndResponse.code == .commandOK else { throw Error.sendEmailError(dataEndResponse.response) } print(dataEndResponse.response) // 5 try sendRequest(.quit) }
Отправляем MAIL FROM.
Отдельно отправляем RCPT TO для каждого получателя.
Инициируем старт ввода сообщения.
Отправляем само сообщение.
Завершаем сессию, сообщение успешно отправлено.
На этом клиент для отправки SMTP сообщений готов. Полный код класса. Отмечу, что реализация неполноценная. Нет поддержки всех способов аутентификации, нет поддержки всех SMTP расширений (например PIPELINING), нет возможности отправить вложение. Реализовали заготовку для MVP. Давайте теперь посмотрим как это все выглядит.
Результат
Код всего остального вокруг SMTP разбирать не буду. Кому интересно, можете ознакомиться тут. В этом блоке лишь покажу, что получилось в итоге.
Начинаем с экрана ввода кредов. Запрашиваем у пользователя ввести сервер, username и пароль для него. Пароль сохраняем в keychain, все остальное кладем в defaults (при перезаходе в приложение повторно не запрашиваем данные).
Современные почтовые клиенты не запрашивают ввод почтового сервера, так как они сами пытаются его вычислить по домену из почты, используя Autodiscover (и другие способы). В рамках данного приложения не стал это реализовывать, оставил только ручной ввод сервера.
smtp.mail.ru - для mail
smtp.yandex.ru - для яндекса
smtp.gmail.com - для gmail
Второй экран - отправка письма. Ряд филдов для ввода данных и отправка (через наш сервис)
Это все приложение. На практическом примере познакомились с SMTP протоколом и (с нуля) написали клиент для отправки почтовых сообщений. Клиент еще можно много чем расширить. Реализовать автоопределение сервера, подключить еще одну важную составляющую - получение писем с помощью IMAP, или добавить отправку вложений и поддержку SMTP расширений. Но это уже в следующий раз..
Полезные ресурсы
MFMailComposeViewController (самый простой способ дать пользователю возможность отправить письмо)
