geary/src/engine/smtp/smtp-client-service.vala
Cédric Bellegarde 2a85a24605 engine: Fix fill_outbox_queue() prototype
cancellable can be null.
2023-10-02 18:37:43 +02:00

461 lines
17 KiB
Vala

/*
* Copyright © 2016 Software Freedom Conservancy Inc.
* Copyright © 2018, 2020 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Manages connecting to an SMTP network service.
*
* This class maintains a queue of email messages to be delivered, and
* opens SMTP connections to deliver queued messages as needed.
*/
public class Geary.Smtp.ClientService : Geary.ClientService {
/** The GLib logging domain used for SMTP sub-system logging. */
public const string LOGGING_DOMAIN = Logging.DOMAIN + ".Smtp";
/** The GLib logging domain used for SMTP protocol logging. */
public const string PROTOCOL_LOGGING_DOMAIN = Logging.DOMAIN + ".Smtp.Net";
// Used solely for debugging, hence "(no subject)" not marked for
// translation
private static string email_subject(EmailHeaderSet email) {
return (
email.subject != null && !String.is_empty(email.subject.to_string()))
? email.subject.to_string()
: "(no subject)";
}
/** Folder used for storing and retrieving queued mail. */
public Outbox.Folder? outbox { get; internal set; default = null; }
/** Progress monitor indicating when email is being sent. */
public ProgressMonitor sending_monitor {
get;
private set;
default = new SimpleProgressMonitor(ProgressType.ACTIVITY);
}
/** {@inheritDoc} */
public override string logging_domain {
get { return LOGGING_DOMAIN; }
}
private Account owner { get { return this.outbox.account; } }
private Nonblocking.Queue<EmailIdentifier> outbox_queue =
new Nonblocking.Queue<EmailIdentifier>.fifo();
private Cancellable? queue_cancellable = null;
/** Emitted when the manager has sent an email. */
public signal void email_sent(Geary.Email email);
/** Emitted when an error occurred sending an email. */
public signal void report_problem(Geary.ProblemReport problem);
public ClientService(AccountInformation account,
ServiceInformation service,
Endpoint remote) {
base(account, service, remote);
}
/**
* Starts the manager opening IMAP client sessions.
*/
public override async void start(GLib.Cancellable? cancellable = null)
throws GLib.Error {
yield this.outbox.open_async(Folder.OpenFlags.NONE, cancellable);
yield this.fill_outbox_queue(cancellable);
notify_started();
}
/**
* Stops the manager running, closing any existing sessions.
*/
public override async void stop(GLib.Cancellable? cancellable = null)
throws GLib.Error {
notify_stopped();
this.stop_postie();
// Wait for the postie to actually stop before closing the
// folder so w don't interrupt e.g. sending/saving/deleting
// mail
while (this.queue_cancellable != null) {
GLib.Idle.add(this.stop.callback);
yield;
}
yield this.outbox.close_async(cancellable);
}
/**
* Saves and queues email for immediate delivery.
*
* This is a convenience method that calls {@link save_email} then
* {@link queue_email} with the resulting id.
*/
public async void send_email(Geary.ComposedEmail composed,
GLib.Cancellable? cancellable)
throws GLib.Error {
queue_email(yield save_email(composed, cancellable));
}
/**
* Saves a composed email in the outbox.
*
* This sets a suitable MessageID header for the message, then
* saves the updated message in {@link outbox}. Returns the
* identifier for the saved email, suitable for use with {@link
* queue_email}.
*
* @see send_email
*/
public async EmailIdentifier save_email(Geary.ComposedEmail composed,
GLib.Cancellable? cancellable)
throws GLib.Error {
debug("Saving composed email: %s", email_subject(composed));
// XXX work out what our public IP address is somehow and use
// that in preference to the originator's domain
var from = composed.from;
var domain = from != null && !from.is_empty
? from[0].domain
: this.account.primary_mailbox.domain;
Geary.RFC822.Message rfc822 =
yield new Geary.RFC822.Message.from_composed_email(
composed, GMime.utils_generate_message_id(domain), cancellable
);
EmailIdentifier id = yield this.outbox.create_email_async(
rfc822, null, null, cancellable
);
debug("Saved composed email as %s", id.to_string());
return id;
}
/**
* Queues an email for immediate delivery.
*
* The given identifier must be for {@link outbox}, for example as
* given by {@link save_email}.
*
* @see send_email
*/
public void queue_email(EmailIdentifier outbox_identifier) {
debug("Queuing email for sending: %s", outbox_identifier.to_string());
this.outbox_queue.send(outbox_identifier);
}
/** Starts the postie delivering messages. */
protected override void became_reachable() {
this.start_postie.begin();
}
/** Stops the postie delivering. */
protected override void became_unreachable() {
this.stop_postie();
}
/**
* Starts delivery of messages in the queue.
*/
private async void start_postie() {
debug("Starting outbox postie with %u messages queued", this.outbox_queue.size);
if (this.queue_cancellable != null) {
return;
}
Cancellable cancellable = this.queue_cancellable =
new GLib.Cancellable();
// Start the send queue.
while (!cancellable.is_cancelled()) {
// yield until a message is ready
EmailIdentifier id = null;
bool email_handled = false;
try {
id = yield this.outbox_queue.receive(cancellable);
yield process_email(id, cancellable);
email_handled = true;
} catch (SmtpError err) {
if (err is SmtpError.AUTHENTICATION_FAILED) {
notify_authentication_failed();
} else if (err is SmtpError.STARTTLS_FAILED ||
err is SmtpError.NOT_CONNECTED) {
notify_connection_failed(new ErrorContext(err));
} else if (err is SmtpError.PARSE_ERROR ||
err is SmtpError.SERVER_ERROR ||
err is SmtpError.NOT_SUPPORTED) {
notify_unrecoverable_error(new ErrorContext(err));
}
cancellable.cancel();
} catch (GLib.IOError.CANCELLED err) {
// Nothing to do here — we're already cancelled.
} catch (EngineError.NOT_FOUND err) {
email_handled = true;
debug("Queued email %s not found in outbox, ignoring: %s",
id.to_string(), err.message);
} catch (GLib.Error err) {
notify_connection_failed(new ErrorContext(err));
cancellable.cancel();
}
if (!email_handled && id != null) {
// Send was bad, try sending again later
this.outbox_queue.send(id);
}
}
this.queue_cancellable = null;
debug("Outbox postie exited");
}
/**
* Stops delivery of messages in the queue.
*/
private void stop_postie() {
debug("Stopping outbox postie");
if (this.queue_cancellable != null) {
this.queue_cancellable.cancel();
}
}
/**
* Loads any email in the outbox and adds them to the queue.
*/
private async void fill_outbox_queue(GLib.Cancellable? cancellable) {
debug("Filling queue");
try {
Gee.List<Email>? queued = yield this.outbox.list_email_by_id_async(
null,
int.MAX, // fetch all
Email.Field.NONE, // ids only
Folder.ListFlags.OLDEST_TO_NEWEST,
cancellable
);
if (queued != null) {
foreach (Email email in queued) {
this.outbox_queue.send(email.id);
}
}
} catch (Error err) {
warning("Error filling queue: %s", err.message);
}
}
// Returns true if email was successfully processed, else false
private async void process_email(EmailIdentifier id, Cancellable cancellable)
throws GLib.Error {
// To prevent spurious connection failures, ensure tokens are
// up-to-date before attempting to send the email
if (!yield this.account.load_outgoing_credentials(cancellable)) {
throw new SmtpError.AUTHENTICATION_FAILED("Credentials not loaded");
}
Email email = yield this.outbox.fetch_email_async(
id, Email.Field.ALL, Folder.ListFlags.NONE, cancellable
);
if (!email.email_flags.contains(EmailFlags.OUTBOX_SENT)) {
RFC822.Message message = email.get_message();
debug("Outbox postie: Sending \"%s\" (ID:%s)...",
email_subject(message), email.id.to_string());
yield send_email_internal(message, cancellable);
email_sent(email);
// Mark as sent, so if there's a problem pushing up to
// Sent, we don't retry sending. Don't pass the
// cancellable here - if it's been sent we want to try to
// update the sent flag anyway
debug("Outbox postie: Marking %s as sent", email.id.to_string());
Geary.EmailFlags flags = new Geary.EmailFlags();
flags.add(Geary.EmailFlags.OUTBOX_SENT);
yield this.outbox.mark_email_async(
Collection.single(email.id), flags, null, null
);
if (cancellable.is_cancelled()) {
throw new GLib.IOError.CANCELLED("Send has been cancelled");
}
}
// If we get to this point, the message has either been just
// sent, or previously sent but not saved. So now try flagging
// as such and saving it if enabled, else sync the folder in
// case the provider saved it so the new mail shows up.
if (this.account.save_sent) {
debug("Outbox postie: Saving %s to sent mail",
email.id.to_string());
yield save_sent_mail(email, cancellable);
} else {
debug("Outbox postie: Syncing sent mail to find %s",
email.id.to_string());
yield sync_sent_mail(email, cancellable);
}
// Again, don't observe the cancellable here - if it's been
// send and saved we want to try to remove it anyway.
debug("Outbox postie: Deleting row %s", email.id.to_string());
yield this.outbox.remove_email_async(Collection.single(email.id), null);
}
private async void send_email_internal(Geary.RFC822.Message rfc822, Cancellable? cancellable)
throws Error {
Credentials? login = this.account.get_outgoing_credentials();
if (login != null && !login.is_complete()) {
throw new SmtpError.AUTHENTICATION_FAILED("Token not loaded");
}
Smtp.ClientSession smtp = new Geary.Smtp.ClientSession(this.remote);
smtp.set_logging_parent(this);
sending_monitor.notify_start();
Error? smtp_err = null;
try {
yield smtp.login_async(login, cancellable);
} catch (Error login_err) {
debug("SMTP login error: %s", login_err.message);
smtp_err = login_err;
}
if (smtp_err == null) {
// Determine the SMTP reverse path, this gets used for
// bounce notifications, etc. Use the sender by default,
// since if specified the message is explicitly being sent
// on behalf of someone else.
RFC822.MailboxAddress? reverse_path = rfc822.sender;
if (reverse_path == null) {
// If no sender specified, use the first from address
// that is accountured for this account.
if (rfc822.from != null) {
foreach (RFC822.MailboxAddress from in rfc822.from) {
if (this.account.has_sender_mailbox(from)) {
reverse_path = from;
break;
}
}
}
if (reverse_path == null) {
// Fall back to using the account's primary
// mailbox if nether a sender nor a from address
// from this account is found.
reverse_path = this.account.primary_mailbox;
}
}
// Now send it
try {
yield smtp.send_email_async(reverse_path, rfc822, cancellable);
} catch (Error send_err) {
debug("SMTP send mail error: %s", send_err.message);
smtp_err = send_err;
}
}
try {
// always logout
yield smtp.logout_async(false, null);
} catch (Error err) {
debug("Unable to disconnect from SMTP server %s: %s", smtp.to_string(), err.message);
}
sending_monitor.notify_finish();
if (smtp_err != null)
throw smtp_err;
}
private async void save_sent_mail(Geary.Email message,
GLib.Cancellable? cancellable)
throws GLib.Error {
Geary.FolderSupport.Create? create = (
yield this.owner.get_required_special_folder_async(
SENT, cancellable
)
) as Geary.FolderSupport.Create;
if (create == null) {
throw new EngineError.UNSUPPORTED(
"Save sent mail enabled, but no writable sent mail folder"
);
}
RFC822.Message raw = message.get_message();
bool open = false;
try {
yield create.open_async(NO_DELAY, cancellable);
open = true;
yield create.create_email_async(raw, null, null, cancellable);
yield wait_for_message(create, message, cancellable);
} finally {
if (open) {
try {
yield create.close_async(null);
} catch (Error e) {
debug("Error closing folder %s: %s", create.to_string(), e.message);
}
}
}
}
private async void sync_sent_mail(Geary.Email message,
GLib.Cancellable? cancellable)
throws GLib.Error {
Geary.Folder sent = this.owner.get_special_folder(SENT);
if (sent != null) {
bool open = false;
try {
yield sent.open_async(NO_DELAY, cancellable);
open = true;
yield sent.synchronise_remote(cancellable);
yield wait_for_message(sent, message, cancellable);
} finally {
if (open) {
try {
yield sent.close_async(null);
} catch (Error e) {
debug("Error closing folder %s: %s",
sent.to_string(), e.message);
}
}
}
}
}
// Wait for a sent message to turn up. There's no guarantee how or
// when a server may make newly saved email show up, so poll for
// it. :(
private async void wait_for_message(Folder location,
Email sent,
GLib.Cancellable cancellable)
throws GLib.Error {
RFC822.MessageID? id = sent.message_id;
if (id != null) {
const int MAX_RETRIES = 3;
for (int i = 0; i < MAX_RETRIES; i++) {
Gee.List<Email>? list = yield location.list_email_by_id_async(
null, 1, REFERENCES, NONE, cancellable
);
if (list != null && !list.is_empty) {
Email listed = Collection.first(list);
if (listed.message_id != null &&
listed.message_id.equal_to(id)) {
break;
}
}
// Wait a second before retrying to give the server
// some breathing room
debug("Waiting for sent mail...");
GLib.Timeout.add_seconds(1, wait_for_message.callback);
yield;
}
}
}
}