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.
This commit is contained in:
Michael Gratton 2020-05-06 14:44:44 +10:00 committed by Michael James Gratton
parent 26e76b6f4d
commit c54ff85ccd
6 changed files with 118 additions and 81 deletions

View file

@ -1069,7 +1069,7 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
MailboxSpecifier mailbox = session.get_mailbox_for_path(this.folder.path);
AppendCommand cmd = new AppendCommand(
mailbox, msg_flags, internaldate, message.get_network_buffer(false)
mailbox, msg_flags, internaldate, message.get_rfc822_buffer()
);
Gee.Map<Command, StatusResponse> responses = yield exec_commands_async(

View file

@ -122,7 +122,7 @@ public class Geary.Outbox.Folder :
// 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_network_buffer(false));
stmt.bind_string_buffer(0, rfc822.get_rfc822_buffer());
stmt.bind_int64(1, ordering);
int64 new_id = stmt.exec_insert(cancellable);

View file

@ -16,6 +16,7 @@
*/
public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
/**
* Callback for including non-text MIME entities in message bodies.
*
@ -30,11 +31,36 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
*/
public delegate string? InlinePartReplacer(Part part);
private const string HEADER_IN_REPLY_TO = "In-Reply-To";
private const string HEADER_REFERENCES = "References";
private const string HEADER_MAILER = "X-Mailer";
private const string HEADER_BCC = "Bcc";
/** Options to use when serialising a message in RFC 822 format. */
[Flags]
public enum RFC822FormatOptions {
/** Format for RFC 822 in general. */
NONE,
/**
* The message should be serialised for transmission via SMTP.
*
* SMTP imposes both operational and data-format requirements
* on RFC 822 style messages. In particular, BCC headers
* should not be included since they will expose BCC
* recipients, and lines must be dot-stuffed so as to avoid
* terminating the message early if a line starting with a `.`
* is encountered.
*
* See [[http://tools.ietf.org/html/rfc5321#section-4.5.2]]
*/
SMTP_FORMAT;
}
// Internal note: If a header field is added here, it *must* be
// set in Message.from_gmime_message(), below.
@ -440,22 +466,6 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
this.message.set_mime_part(main_part);
}
// Makes a copy of the given message without the BCC fields. This is used for sending the email
// without sending the BCC headers to all recipients.
public Message.without_bcc(Message email) throws GLib.Error {
// GMime doesn't make it easy to get a copy of the body of a message. It's easy to
// make a new message and add in all the headers, but calling set_mime_part() with
// the existing one's get_mime_part() result yields a double Content-Type header in
// the *original* message. Clearly the objects aren't meant to be used like that.
// Barring any better way to clone a message, which I couldn't find by looking at
// the docs, we just dump out the old message to a buffer and read it back in to
// create the new object. Kinda sucks, but our hands are tied.
this.from_buffer(email.message_to_memory_buffer(false, false));
this.message.remove_header(HEADER_BCC);
this.bcc = null;
}
private GMime.Object? coalesce_related(Gee.List<GMime.Object> parts,
string type) {
GMime.Object? part = coalesce_parts(parts, "related");
@ -649,22 +659,21 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
}
/**
* Returns the {@link Message} as a {@link Memory.Buffer} suitable for in-memory use (i.e.
* with native linefeed characters).
* Serialises the message using native (i.e. LF) line endings.
*/
public Memory.Buffer get_native_buffer() throws Error {
return message_to_memory_buffer(false, false);
return message_to_memory_buffer(false, NONE);
}
/**
* Returns the {@link Message} as a {@link Memory.Buffer} suitable for transmission or
* storage (i.e. using protocol-specific linefeeds).
* Serialises the message using RFC 822 (i.e. CRLF) line endings.
*
* The buffer can also be dot-stuffed if required. See
* [[http://tools.ietf.org/html/rfc2821#section-4.5.2]]
* Returns the message as a memory buffer suitable for network
* transmission and interoperability with other RFC 822 consumers.
*/
public Memory.Buffer get_network_buffer(bool dotstuffed) throws Error {
return message_to_memory_buffer(true, dotstuffed);
public Memory.Buffer get_rfc822_buffer(RFC822FormatOptions options = NONE)
throws Error {
return message_to_memory_buffer(true, options);
}
/**
@ -1084,7 +1093,7 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
}
private Memory.Buffer message_to_memory_buffer(bool encode_lf,
bool stuff_smtp)
RFC822FormatOptions options)
throws Error {
ByteArray byte_array = new ByteArray();
GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array);
@ -1096,18 +1105,33 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
} else {
stream_filter.add(new GMime.FilterDos2Unix(false));
}
if (stuff_smtp) {
if (RFC822FormatOptions.SMTP_FORMAT in options) {
stream_filter.add(new GMime.FilterSmtpData());
}
if (message.write_to_stream(Geary.RFC822.get_format_options(), stream_filter) < 0)
throw new Error.FAILED("Unable to write RFC822 message to filter stream");
var format = Geary.RFC822.get_format_options();
if (RFC822FormatOptions.SMTP_FORMAT in options) {
format = format.clone();
format.add_hidden_header("Bcc");
}
if (stream_filter.flush() != 0)
throw new Error.FAILED("Unable to flush RFC822 message to memory stream");
if (message.write_to_stream(format, stream_filter) < 0) {
throw new Error.FAILED(
"Unable to write RFC822 message to filter stream"
);
}
if (stream.flush() != 0)
throw new Error.FAILED("Unable to flush RFC822 message to memory buffer");
if (stream_filter.flush() != 0) {
throw new Error.FAILED(
"Unable to flush RFC822 message to memory stream"
);
}
if (stream.flush() != 0) {
throw new Error.FAILED(
"Unable to flush RFC822 message to memory buffer"
);
}
return new Memory.ByteBuffer.from_byte_array(byte_array);
}

View file

@ -119,47 +119,28 @@ internal class Geary.Smtp.ClientConnection : BaseObject, Logging.Source {
* Returns the final Response of the transaction. If the ResponseCode is not a successful
* completion, the message should not be considered sent.
*/
public async Response send_data_async(Memory.Buffer data, bool already_dotstuffed,
Cancellable? cancellable = null) throws Error {
public async Response send_data_async(Memory.Buffer data,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_connected();
// In the case of DATA, want to receive an intermediate response code, specifically 354
Response response = yield transaction_async(new Request(Command.DATA), cancellable);
if (!response.code.is_start_data())
return response;
if (response.code.is_start_data()) {
debug("SMTP Data: <%z>", data.size);
debug("SMTP Data: <%z>", data.size);
if (!already_dotstuffed) {
// By using DataStreamNewlineType.ANY, we're assured to get each line and convert to
// a proper line terminator for SMTP
DataInputStream dins = new DataInputStream(data.get_input_stream());
dins.set_newline_type(DataStreamNewlineType.ANY);
// Read each line and dot-stuff if necessary
for (;;) {
size_t length;
string? line = yield dins.read_line_async(Priority.DEFAULT, cancellable, out length);
if (line == null)
break;
// stuffing
if (line[0] == '.')
yield Stream.write_string_async(douts, ".", cancellable);
yield Stream.write_string_async(douts, line, cancellable);
yield Stream.write_string_async(douts, DataFormat.LINE_TERMINATOR, cancellable);
}
} else {
// ready to go, send and commit
yield Stream.write_all_async(douts, data, cancellable);
yield Stream.write_all_async(this.douts, data, cancellable);
// terminate buffer and flush to server
yield Stream.write_string_async(
this.douts, DataFormat.DATA_TERMINATOR, cancellable
);
yield this.douts.flush_async(Priority.DEFAULT, cancellable);
response = yield recv_response_async(cancellable);
}
// terminate buffer and flush to server
yield Stream.write_string_async(douts, DataFormat.DATA_TERMINATOR, cancellable);
yield douts.flush_async(Priority.DEFAULT, cancellable);
return yield recv_response_async(cancellable);
return response;
}
public async void send_request_async(Request request, Cancellable? cancellable = null) throws Error {

View file

@ -214,9 +214,9 @@ public class Geary.Smtp.ClientSession : BaseObject, Logging.Source {
yield send_rcpts_async(addrlist, cancellable);
// DATA
Geary.RFC822.Message email_copy = new Geary.RFC822.Message.without_bcc(email);
response = yield cx.send_data_async(email_copy.get_network_buffer(true), true,
cancellable);
response = yield cx.send_data_async(
email.get_rfc822_buffer(), cancellable
);
if (!response.code.is_success_completed())
response.throw_error("Unable to send message");

View file

@ -59,9 +59,10 @@ This is the second line.
add_test("get_searchable_recipients", get_searchable_recipients);
add_test("from_composed_email", from_composed_email);
add_test("from_composed_email_inline_attachments", from_composed_email_inline_attachments);
add_test("get_network_buffer", get_network_buffer);
add_test("get_network_buffer_dot_stuff", get_network_buffer_dot_stuff);
add_test("get_network_buffer_long_ascii_line", get_network_buffer_long_ascii_line);
add_test("get_rfc822_buffer", get_rfc822_buffer);
add_test("get_rfc822_buffer_dot_stuff", get_rfc822_buffer_dot_stuff);
add_test("get_rfc822_buffer_no_bcc", get_rfc822_buffer_no_bcc);
add_test("get_rfc822_buffer_long_ascii_line", get_rfc822_buffer_long_ascii_line);
}
public void basic_message_from_buffer() throws GLib.Error {
@ -210,13 +211,13 @@ This is the second line.
assert_true(searchable.contains("Jane Doe BCC <jdoe_bcc@somewhere.tld>"), "Expected bcc address");
}
public void get_network_buffer() throws GLib.Error {
public void get_rfc822_buffer() throws GLib.Error {
Message test = resource_to_message(BASIC_TEXT_PLAIN);
Memory.Buffer buffer = test.get_network_buffer(true);
Memory.Buffer buffer = test.get_rfc822_buffer(NONE);
assert_true(buffer.to_string() == NETWORK_BUFFER_EXPECTED, "Network buffer differs");
}
public void get_network_buffer_dot_stuff() throws GLib.Error {
public void get_rfc822_buffer_dot_stuff() throws GLib.Error {
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
"Test", "test@example.com"
);
@ -235,11 +236,42 @@ This is the second line.
);
Geary.RFC822.Message message = message_from_composed_email.end(async_result());
string message_data = message.get_network_buffer(true).to_string();
string message_data = message.get_rfc822_buffer(SMTP_FORMAT).to_string();
assert_true(message_data.has_suffix("..newline\r\n..\r\n"));
}
public void get_network_buffer_long_ascii_line() throws GLib.Error {
public void get_rfc822_buffer_no_bcc() throws GLib.Error {
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
"Test", "test@example.com"
);
RFC822.MailboxAddress bcc = new RFC822.MailboxAddress(
"BCC", "bcc@example.com"
);
RFC822.MailboxAddress from = new RFC822.MailboxAddress(
"Sender", "sender@example.com"
);
Geary.ComposedEmail composed = new Geary.ComposedEmail(
new GLib.DateTime.now_local(),
new Geary.RFC822.MailboxAddresses.single(from)
).set_to(
new Geary.RFC822.MailboxAddresses.single(to)
).set_bcc(
new Geary.RFC822.MailboxAddresses.single(bcc)
);
composed.body_text = "\nbody\n";
this.message_from_composed_email.begin(
composed,
this.async_completion
);
Geary.RFC822.Message message = message_from_composed_email.end(async_result());
string message_data = message.get_rfc822_buffer(SMTP_FORMAT).to_string();
assert_true("To: Test <test@example.com>\r\n" in message_data);
assert_false("bcc" in message_data.down());
}
public void get_rfc822_buffer_long_ascii_line() throws GLib.Error {
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
"Test", "test@example.com"
);
@ -265,7 +297,7 @@ This is the second line.
);
Geary.RFC822.Message message = message_from_composed_email.end(async_result());
string message_data = message.get_network_buffer(true).to_string();
string message_data = message.get_rfc822_buffer(SMTP_FORMAT).to_string();
foreach (var line in message_data.split("\n")) {
assert_true(line.length < 1000, line);
}