geary/src/engine/outbox/outbox-folder.vala
Michael Gratton c54ff85ccd Geary.RFC822.Message: Update API for SMTP formatting
Remove `Message.without_bcc` ctor since we can now get GMime to exclude
BCC headers when serialising, avoiding the overhead of taking a complete
copy of the message just to strip BCCs.

Rename `get_network_buffer` to `get_rfc822_buffer` to be a bit more
explicit in that's the way to get an actual RFC822 formatted message.
Replace book args with flags, and take a protocol-specific approach
rather than feature-specific, because in the end you're either going to
want all the SMTP formatting quirks or none of them.

Remove custom dot-stuffing support from Smtp.ClientConnection, since it
is redundant.
2020-05-06 14:58:56 +10:00

501 lines
17 KiB
Vala

/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2017-2018 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.
*/
/**
* A folder for storing outgoing mail.
*/
public class Geary.Outbox.Folder :
Geary.AbstractLocalFolder,
Geary.FolderSupport.Create,
Geary.FolderSupport.Mark,
Geary.FolderSupport.Remove {
/** The canonical name of the outbox folder. */
public const string MAGIC_BASENAME = "$GearyOutbox$";
private class OutboxRow {
public int64 id;
public int position;
public int64 ordering;
public bool sent;
public Memory.Buffer? message;
public EmailIdentifier outbox_id;
public OutboxRow(int64 id, int position, int64 ordering, bool sent, Memory.Buffer? message) {
assert(position >= 1);
this.id = id;
this.position = position;
this.ordering = ordering;
this.sent = sent;
this.message = message;
outbox_id = new EmailIdentifier(id, ordering);
}
}
/** {@inheritDoc} */
public override Account account { get { return this._account; } }
/** {@inheritDoc} */
public override Geary.FolderProperties properties {
get { return _properties; }
}
/**
* Returns the path to this folder.
*
* This is always the child of the root given to the constructor,
* with the name given by {@link MAGIC_BASENAME}.
*/
public override FolderPath path {
get {
return _path;
}
}
private FolderPath _path;
/**
* Returns the type of this folder.
*
* This is always {@link Folder.SpecialUse.OUTBOX}
*/
public override Geary.Folder.SpecialUse used_as {
get {
return OUTBOX;
}
}
private weak Account _account;
private weak ImapDB.Account local;
private Db.Database? db = null;
private FolderProperties _properties = new FolderProperties(0, 0);
private int64 next_ordering = 0;
internal Folder(Account account, FolderRoot root, ImapDB.Account local) {
this._account = account;
this._path = root.get_child(MAGIC_BASENAME, Trillian.TRUE);
this.local = local;
}
public override async bool open_async(Geary.Folder.OpenFlags open_flags,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
bool opened = yield base.open_async(open_flags, cancellable);
if (opened) {
this.db = this.local.db;
}
return opened;
}
public override async bool close_async(GLib.Cancellable? cancellable = null)
throws GLib.Error {
bool closed = yield base.close_async(cancellable);
if (closed) {
this.db = null;
}
return closed;
}
public virtual async Geary.EmailIdentifier?
create_email_async(RFC822.Message rfc822,
Geary.EmailFlags? flags,
GLib.DateTime? date_received,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
int email_count = 0;
OutboxRow? row = null;
yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
int64 ordering = do_get_next_ordering(cx, cancellable);
// save in database ready for SMTP, but without dot-stuffing
Db.Statement stmt = cx.prepare(
"INSERT INTO SmtpOutboxTable (message, ordering) VALUES (?, ?)");
stmt.bind_string_buffer(0, rfc822.get_rfc822_buffer());
stmt.bind_int64(1, ordering);
int64 new_id = stmt.exec_insert(cancellable);
int position = do_get_position_by_ordering(cx, ordering, cancellable);
row = new OutboxRow(new_id, position, ordering, false, null);
email_count = do_get_email_count(cx, cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
// update properties
_properties.set_total(yield get_email_count_async(cancellable));
Gee.List<EmailIdentifier> list = new Gee.ArrayList<EmailIdentifier>();
list.add(row.outbox_id);
notify_email_appended(list);
notify_email_locally_appended(list);
notify_email_count_changed(email_count, CountChangeReason.APPENDED);
return row.outbox_id;
}
public virtual async void
mark_email_async(Gee.Collection<Geary.EmailIdentifier> to_mark,
EmailFlags? flags_to_add,
EmailFlags? flags_to_remove,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
Gee.Map<Geary.EmailIdentifier,EmailFlags> changed =
new Gee.HashMap<Geary.EmailIdentifier,EmailFlags>();
foreach (Geary.EmailIdentifier id in to_mark) {
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id != null) {
yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
do_mark_email_as_sent(cx, outbox_id, cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable
);
changed.set(id, flags_to_add);
}
}
notify_email_flags_changed(changed);
}
public virtual async void
remove_email_async(Gee.Collection<Geary.EmailIdentifier> email_ids,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
Gee.List<Geary.EmailIdentifier> removed = new Gee.ArrayList<Geary.EmailIdentifier>();
int final_count = 0;
yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
foreach (Geary.EmailIdentifier id in email_ids) {
// ignore anything not belonging to the outbox, but also don't report it as removed
// either
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id == null)
continue;
// Even though we discard the new value here, this check must
// occur before any insert/delete on the table, to ensure we
// never reuse an ordering value while Geary is running.
do_get_next_ordering(cx, cancellable);
if (do_remove_email(cx, outbox_id, cancellable))
removed.add(outbox_id);
}
final_count = do_get_email_count(cx, cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
if (removed.size >= 0) {
_properties.set_total(final_count);
notify_email_removed(removed);
notify_email_count_changed(final_count, CountChangeReason.REMOVED);
}
}
public override async Gee.List<Email>?
list_email_by_id_async(Geary.EmailIdentifier? _initial_id,
int count,
Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
EmailIdentifier? initial_id = _initial_id as EmailIdentifier;
if (_initial_id != null && initial_id == null) {
throw new EngineError.BAD_PARAMETERS("EmailIdentifier %s not for Outbox",
initial_id.to_string());
}
if (count <= 0)
return null;
bool list_all = (required_fields != Email.Field.NONE);
string select = "id, ordering";
if (list_all) {
select = select + ", message, sent";
}
Gee.List<Geary.Email>? list = null;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
string dir = flags.is_newest_to_oldest() ? "DESC" : "ASC";
Db.Statement stmt;
if (initial_id != null) {
stmt = cx.prepare("""
SELECT %s
FROM SmtpOutboxTable
WHERE ordering >= ?
ORDER BY ordering %s
LIMIT ?
""".printf(select ,dir));
stmt.bind_int64(0,
flags.is_including_id() ? initial_id.ordering : initial_id.ordering + 1);
stmt.bind_int(1, count);
} else {
stmt = cx.prepare("""
SELECT %s
FROM SmtpOutboxTable
ORDER BY ordering %s
LIMIT ?
""".printf(select, dir));
stmt.bind_int(0, count);
}
Db.Result results = stmt.exec(cancellable);
if (results.finished)
return Db.TransactionOutcome.DONE;
list = new Gee.ArrayList<Geary.Email>();
int position = -1;
do {
int64 ordering = results.int64_at(1);
if (position == -1) {
position = do_get_position_by_ordering(
cx, ordering, cancellable
);
assert(position >= 1);
}
list.add(
row_to_email(
new OutboxRow(
results.rowid_at(0),
position,
ordering,
list_all ? results.bool_at(3) : false,
list_all ? results.string_buffer_at(2) : null
)
)
);
position += flags.is_newest_to_oldest() ? -1 : 1;
assert(position >= 1);
} while (results.next());
return Db.TransactionOutcome.DONE;
}, cancellable);
return list;
}
public override async Gee.List<Geary.Email>?
list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
Gee.List<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
foreach (Geary.EmailIdentifier id in ids) {
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id == null)
throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string());
OutboxRow? row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable);
if (row == null)
continue;
list.add(row_to_email(row));
}
return Db.TransactionOutcome.DONE;
}, cancellable);
return (list.size > 0) ? list : null;
}
public override async Email
fetch_email_async(Geary.EmailIdentifier id,
Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id == null)
throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string());
OutboxRow? row = null;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable);
return Db.TransactionOutcome.DONE;
}, cancellable);
if (row == null)
throw new EngineError.NOT_FOUND("No message with ID %s in outbox", id.to_string());
return row_to_email(row);
}
public override void set_used_as_custom(bool enabled)
throws EngineError.UNSUPPORTED {
throw new EngineError.UNSUPPORTED("Folder special use cannot be changed");
}
internal async void
add_to_containing_folders_async(Gee.Collection<Geary.EmailIdentifier> ids,
Gee.MultiMap<Geary.EmailIdentifier,FolderPath> map,
GLib.Cancellable? cancellable)
throws GLib.Error {
check_open();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
foreach (Geary.EmailIdentifier id in ids) {
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id == null)
continue;
OutboxRow? row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable);
if (row == null)
continue;
map.set(id, path);
}
return Db.TransactionOutcome.DONE;
}, cancellable);
}
// Utility for getting an email object back from an outbox row.
private Geary.Email row_to_email(OutboxRow row) throws Error {
Geary.Email? email = null;
// If the row doesn't contain any message, just the id will do
if (row.message == null) {
email = new Email(row.outbox_id);
} else {
RFC822.Message message = new RFC822.Message.from_buffer(row.message);
email = message.get_email(row.outbox_id);
// TODO: Determine message's total size (header + body) to
// store in Properties.
email.set_email_properties(
new EmailProperties(new DateTime.now_local(), -1)
);
Geary.EmailFlags flags = new Geary.EmailFlags();
if (row.sent)
flags.add(Geary.EmailFlags.OUTBOX_SENT);
email.set_flags(flags);
}
return email;
}
private async int get_email_count_async(Cancellable? cancellable) throws Error {
int count = 0;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
count = do_get_email_count(cx, cancellable);
return Db.TransactionOutcome.DONE;
}, cancellable);
return count;
}
//
// Transaction helper methods
//
private int64 do_get_next_ordering(Db.Connection cx, Cancellable? cancellable) throws Error {
lock (next_ordering) {
if (next_ordering == 0) {
Db.Statement stmt = cx.prepare("SELECT COALESCE(MAX(ordering), 0) + 1 FROM SmtpOutboxTable");
Db.Result result = stmt.exec(cancellable);
if (!result.finished)
next_ordering = result.int64_at(0);
assert(next_ordering > 0);
}
return next_ordering++;
}
}
private int do_get_email_count(Db.Connection cx, Cancellable? cancellable) throws Error {
Db.Statement stmt = cx.prepare("SELECT COUNT(*) FROM SmtpOutboxTable");
Db.Result results = stmt.exec(cancellable);
return (!results.finished) ? results.int_at(0) : 0;
}
private int do_get_position_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare(
"SELECT COUNT(*), MAX(ordering) FROM SmtpOutboxTable WHERE ordering <= ? ORDER BY ordering ASC");
stmt.bind_int64(0, ordering);
Db.Result results = stmt.exec(cancellable);
if (results.finished)
return -1;
// without the MAX it's possible to overshoot, so the MAX(ordering) *must* match the argument
if (results.int64_at(1) != ordering)
return -1;
return results.int_at(0) + 1;
}
private OutboxRow? do_fetch_row_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare("""
SELECT id, message, sent
FROM SmtpOutboxTable
WHERE ordering=?
""");
stmt.bind_int64(0, ordering);
Db.Result results = stmt.exec(cancellable);
if (results.finished)
return null;
int position = do_get_position_by_ordering(cx, ordering, cancellable);
if (position < 1)
return null;
return new OutboxRow(results.rowid_at(0), position, ordering, results.bool_at(2),
results.string_buffer_at(1));
}
private void do_mark_email_as_sent(Db.Connection cx,
EmailIdentifier id,
Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare("UPDATE SmtpOutboxTable SET sent = 1 WHERE ordering = ?");
stmt.bind_int64(0, id.ordering);
stmt.exec(cancellable);
}
private bool do_remove_email(Db.Connection cx, EmailIdentifier id, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare("DELETE FROM SmtpOutboxTable WHERE ordering=?");
stmt.bind_int64(0, id.ordering);
return stmt.exec_get_modified(cancellable) > 0;
}
}