1/* 2 * Copyright © 2016 Software Freedom Conservancy Inc. 3 * Copyright © 2020 Michael Gratton <mike@vee.net> 4 * 5 * This software is licensed under the GNU Lesser General Public License 6 * (version 2.1 or later). See the COPYING file in this distribution. 7 */ 8 9/** A client connection to a SMTP service. */ 10public class Geary.Smtp.ClientSession : BaseObject, Logging.Source { 11 12 /** {@inheritDoc} */ 13 public override string logging_domain { 14 get { return ClientService.LOGGING_DOMAIN; } 15 } 16 17 /** {@inheritDoc} */ 18 public Logging.Source? logging_parent { get { return _logging_parent; } } 19 private weak Logging.Source? _logging_parent = null; 20 21 private ClientConnection cx; 22 private bool rset_required = false; 23 24 public virtual signal void connected(Greeting greeting) { 25 } 26 27 public virtual signal void authenticated(Authenticator authenticator) { 28 } 29 30 public virtual signal void disconnected() { 31 } 32 33 public ClientSession(Geary.Endpoint endpoint) { 34 this.cx = new ClientConnection(endpoint); 35 this.cx.set_logging_parent(this); 36 } 37 38 protected virtual void notify_connected(Greeting greeting) { 39 connected(greeting); 40 } 41 42 protected virtual void notify_authenticated(Authenticator authenticator) { 43 authenticated(authenticator); 44 } 45 46 protected virtual void notify_disconnected() { 47 disconnected(); 48 } 49 50 public async Greeting? login_async(Credentials? creds, Cancellable? cancellable = null) throws Error { 51 if (cx.is_connected()) 52 throw new SmtpError.ALREADY_CONNECTED("Connection to %s already exists", to_string()); 53 54 // Greet the SMTP server. 55 Greeting? greeting = yield cx.connect_async(cancellable); 56 if (greeting == null) 57 throw new SmtpError.ALREADY_CONNECTED("Connection to %s already exists", to_string()); 58 yield cx.establish_connection_async(cancellable); 59 60 notify_connected(greeting); 61 62 // authenticate if credentials supplied (they should be if ESMTP is supported) 63 if (creds != null) 64 notify_authenticated(yield attempt_authentication_async(creds, cancellable)); 65 66 return greeting; 67 } 68 69 // Returns authenticator used for successful authentication, otherwise throws exception 70 private async Authenticator attempt_authentication_async(Credentials creds, Cancellable? cancellable) 71 throws Error { 72 // build an authentication style ordering to attempt, going 73 // from reported capabilities to standard fallbacks, while 74 // avoiding repetition ... this is necessary due to server 75 // bugs that report an authentication type is available but 76 // actually isn't, see 77 // 78 // http://redmine.yorba.org/issues/6091 79 // 80 // and 81 // 82 // http://comments.gmane.org/gmane.mail.pine.general/4004 83 Gee.ArrayList<string> auth_order = new Gee.ArrayList<string>(String.stri_equal); 84 85 switch (creds.supported_method) { 86 case Credentials.Method.PASSWORD: 87 // start with advertised authentication styles, in order of our preference (PLAIN 88 // only requires one round-trip) 89 if (cx.capabilities != null) { 90 if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_PLAIN)) 91 auth_order.add(Capabilities.AUTH_PLAIN); 92 93 if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_LOGIN)) 94 auth_order.add(Capabilities.AUTH_LOGIN); 95 } 96 97 // fallback on commonly-implemented styles, again in our order of preference 98 if (!auth_order.contains(Capabilities.AUTH_PLAIN)) 99 auth_order.add(Capabilities.AUTH_PLAIN); 100 101 if (!auth_order.contains(Capabilities.AUTH_LOGIN)) 102 auth_order.add(Capabilities.AUTH_LOGIN); 103 104 if (auth_order.is_empty) { 105 throw new SmtpError.AUTHENTICATION_FAILED( 106 "Unable to authenticate using PASSWORD credentials against %s", 107 to_string() 108 ); 109 } 110 break; 111 112 case Credentials.Method.OAUTH2: 113 if (cx.capabilities != null && 114 !cx.capabilities.has_setting(Capabilities.AUTH, 115 Capabilities.AUTH_OAUTH2)) { 116 throw new SmtpError.AUTHENTICATION_FAILED( 117 "Unable to authenticate using OAUTH2 credentials against %s", 118 to_string() 119 ); 120 } 121 auth_order.add(Capabilities.AUTH_OAUTH2); 122 break; 123 124 default: 125 throw new SmtpError.AUTHENTICATION_FAILED( 126 "Unsupported auth method: %s", creds.supported_method.to_string() 127 ); 128 } 129 130 // go through the list, in order, until one style is accepted 131 do { 132 Authenticator? authenticator; 133 switch (auth_order.remove_at(0)) { 134 case Capabilities.AUTH_PLAIN: 135 authenticator = new PlainAuthenticator(creds); 136 break; 137 138 case Capabilities.AUTH_LOGIN: 139 authenticator = new LoginAuthenticator(creds); 140 break; 141 142 case Capabilities.AUTH_OAUTH2: 143 authenticator = new OAuth2Authenticator(creds); 144 break; 145 146 default: 147 assert_not_reached(); 148 } 149 150 debug("[%s] Attempting %s authenticator", to_string(), authenticator.to_string()); 151 152 Response response = yield cx.authenticate_async(authenticator, cancellable); 153 if (response.code.is_success_completed()) 154 return authenticator; 155 } while (auth_order.size > 0); 156 157 throw new SmtpError.AUTHENTICATION_FAILED("Unable to authenticate with %s", to_string()); 158 } 159 160 public async Response? logout_async(bool force, Cancellable? cancellable = null) throws Error { 161 Response? response = null; 162 try { 163 if (!force) 164 response = yield cx.quit_async(cancellable); 165 } catch (Error err) { 166 // catch because although error occurred, still attempt to close the connection 167 message("Unable to QUIT: %s", err.message); 168 } 169 170 try { 171 if (yield cx.disconnect_async(cancellable)) 172 disconnected(); 173 } catch (Error err2) { 174 // again, catch error but still shut down 175 message("Unable to disconnect: %s", err2.message); 176 } 177 178 rset_required = false; 179 180 return response; 181 } 182 183 public async void send_email_async(Geary.RFC822.MailboxAddress reverse_path, 184 Geary.RFC822.Message email, 185 Cancellable? cancellable = null) 186 throws GLib.Error { 187 if (!cx.is_connected()) 188 throw new SmtpError.NOT_CONNECTED("Not connected to %s", to_string()); 189 190 // RSET if required 191 if (rset_required) { 192 Response rset_response = yield cx.transaction_async(new Request(Command.RSET), cancellable); 193 if (!rset_response.code.is_success_completed()) 194 rset_response.throw_error("Unable to RSET"); 195 196 rset_required = false; 197 } 198 199 // MAIL 200 MailRequest mail_request = new MailRequest(reverse_path); 201 Response response = yield cx.transaction_async(mail_request, cancellable); 202 if (!response.code.is_success_completed()) 203 response.throw_error("\"%s\" failed".printf(mail_request.to_string())); 204 205 // at this point in the session state machine, a RSET is required to start a new 206 // transmission if this fails at any point 207 rset_required = true; 208 209 // RCPTs 210 Gee.List<RFC822.MailboxAddress>? addrlist = email.get_recipients(); 211 if (addrlist == null || addrlist.size == 0) 212 throw new SmtpError.REQUIRED_FIELD("No recipients in message"); 213 214 yield send_rcpts_async(addrlist, cancellable); 215 216 // DATA 217 response = yield cx.send_data_async( 218 email.get_rfc822_buffer(SMTP_FORMAT), cancellable 219 ); 220 if (!response.code.is_success_completed()) 221 response.throw_error("Unable to send message"); 222 223 // if message was transmitted successfully, the state machine resets automatically 224 rset_required = false; 225 } 226 227 private async void send_rcpts_async(Gee.List<RFC822.MailboxAddress>? addrlist, 228 Cancellable? cancellable) throws Error { 229 if (addrlist == null) 230 return; 231 232 // TODO: Support mailbox groups 233 foreach (RFC822.MailboxAddress mailbox in addrlist) { 234 RcptRequest rcpt_request = new RcptRequest(mailbox); 235 Response response = yield cx.transaction_async(rcpt_request, cancellable); 236 237 if (!response.code.is_success_completed()) { 238 if (response.code.is_denied()) { 239 response.throw_error("recipient \"%s\" denied by smtp server".printf(rcpt_request.to_string())); 240 } else { 241 response.throw_error("\"%s\" failed".printf(rcpt_request.to_string())); 242 } 243 } 244 } 245 } 246 247 /** {@inheritDoc} */ 248 public virtual Logging.State to_logging_state() { 249 return new Logging.State(this, this.cx.to_string()); 250 } 251 252 /** Sets the service's logging parent. */ 253 internal void set_logging_parent(Logging.Source parent) { 254 this._logging_parent = parent; 255 } 256 257} 258