geary/src/engine/imap-db/imap-db-attachment.vala
Michael Gratton 2cb0a83342 Don't munge Base64 encoded text parts when saving to disk
Avoid doing character encoding conversion and CRLF stripping when saving
text parts to disk. This prevents UTF-8 encoded text attachments from
being broken when base64 encoded and no encoding is specified.

See #362
2019-07-18 14:38:38 +10:00

294 lines
10 KiB
Vala

/*
* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapDB.Attachment : Geary.Attachment {
public const Email.Field REQUIRED_FIELDS = Email.REQUIRED_FOR_MESSAGE;
private const string NULL_FILE_NAME = "none";
internal int64 message_id { get; private set; }
private int64 attachment_id = -1;
private Attachment(int64 message_id,
Mime.ContentType content_type,
string? content_id,
string? content_description,
Mime.ContentDisposition content_disposition,
string? content_filename) {
base(
content_type,
content_id,
content_description,
content_disposition,
content_filename
);
this.message_id = message_id;
}
internal Attachment.from_part(int64 message_id, RFC822.Part part)
throws Error {
Mime.ContentDisposition? disposition = part.content_disposition;
if (disposition == null) {
disposition = new Mime.ContentDisposition.simple(
Geary.Mime.DispositionType.UNSPECIFIED
);
}
this(
message_id,
part.content_type,
part.content_id,
part.content_description,
disposition,
part.get_clean_filename()
);
}
internal Attachment.from_row(Geary.Db.Result result, File attachments_dir)
throws Error {
string? content_filename = result.string_for("filename");
if (content_filename == ImapDB.Attachment.NULL_FILE_NAME) {
// Prior to 0.12, Geary would store the untranslated
// string "none" as the filename when none was
// specified by the MIME content disposition. Check
// for that and clean it up.
content_filename = null;
}
Mime.ContentDisposition disposition = new Mime.ContentDisposition.simple(
Mime.DispositionType.from_int(result.int_for("disposition"))
);
this(
result.rowid_for("message_id"),
Mime.ContentType.deserialize(result.nonnull_string_for("mime_type")),
result.string_for("content_id"),
result.string_for("description"),
disposition,
content_filename
);
this.attachment_id = result.rowid_for("id");
set_file_info(
generate_file(attachments_dir), result.int64_for("filesize")
);
}
internal void save(Db.Connection cx,
RFC822.Part part,
GLib.File attachments_dir,
Cancellable? cancellable)
throws Error {
insert_db(cx, cancellable);
try {
save_file(part, attachments_dir, cancellable);
update_db(cx, cancellable);
} catch (Error err) {
// Don't honour the cancellable here, it needs to be
// deleted
this.delete(cx, null);
throw err;
}
}
// This isn't async since its only callpaths are via db async
// transactions, which run in independent threads.
internal void delete(Db.Connection cx, Cancellable? cancellable) {
if (this.attachment_id >= 0) {
try {
Db.Statement remove_stmt = cx.prepare(
"DELETE FROM MessageAttachmentTable WHERE id=?");
remove_stmt.bind_rowid(0, this.attachment_id);
remove_stmt.exec();
} catch (Error err) {
debug("Error attempting to remove added attachment row for %s: %s",
this.file.get_path(), err.message);
}
}
if (this.file != null) {
try {
this.file.delete(cancellable);
} catch (Error err) {
debug("Error attempting to remove attachment file %s: %s",
this.file.get_path(), err.message);
}
}
}
private void insert_db(Db.Connection cx, Cancellable? cancellable)
throws Error {
// Insert it into the database.
Db.Statement stmt = cx.prepare("""
INSERT INTO MessageAttachmentTable (message_id, filename, mime_type, filesize, disposition, content_id, description)
VALUES (?, ?, ?, ?, ?, ?, ?)
""");
stmt.bind_rowid(0, this.message_id);
stmt.bind_string(1, this.content_filename);
stmt.bind_string(2, this.content_type.to_string());
stmt.bind_int64(3, 0); // This is updated after saving the file
stmt.bind_int(4, this.content_disposition.disposition_type);
stmt.bind_string(5, this.content_id);
stmt.bind_string(6, this.content_description);
this.attachment_id = stmt.exec_insert(cancellable);
}
// This isn't async since its only callpaths are via db async
// transactions, which run in independent threads
private void save_file(RFC822.Part part,
GLib.File attachments_dir,
Cancellable? cancellable)
throws Error {
if (this.attachment_id < 0) {
throw new IOError.NOT_FOUND("No attachment id assigned");
}
File target = generate_file(attachments_dir);
// create directory, but don't throw exception if already exists
try {
target.get_parent().make_directory_with_parents(cancellable);
} catch (IOError.EXISTS err) {
// All good
}
// Delete any existing file now since we might not be creating
// it again below.
try {
target.delete(cancellable);
} catch (IOError err) {
// All good
}
GLib.OutputStream target_stream = target.create(
FileCreateFlags.NONE, cancellable
);
GMime.Stream stream = new Geary.Stream.MimeOutputStream(
target_stream
);
stream = new GMime.StreamBuffer(
stream, GMime.StreamBufferMode.BLOCK_WRITE
);
part.write_to_stream(stream, RFC822.Part.EncodingConversion.NONE);
// Using the stream's length is a bit of a hack, but at
// least on one system we are getting 0 back for the file
// size if we use target.query_info().
int64 file_size = stream.length();
stream.close();
set_file_info(target, file_size);
}
private void update_db(Db.Connection cx, Cancellable? cancellable)
throws Error {
// Update the file size now we know what it is
Db.Statement stmt = cx.prepare("""
UPDATE MessageAttachmentTable
SET filesize = ?
WHERE id = ?
""");
stmt.bind_int64(0, this.filesize);
stmt.bind_rowid(1, this.attachment_id);
stmt.exec(cancellable);
}
private GLib.File generate_file(GLib.File attachments_dir) {
return attachments_dir
.get_child(this.message_id.to_string())
.get_child(this.attachment_id.to_string())
.get_child(this.content_filename ?? NULL_FILE_NAME);
}
internal static Gee.List<Attachment> save_attachments(Db.Connection cx,
GLib.File attachments_path,
int64 message_id,
Gee.List<RFC822.Part> attachments,
Cancellable? cancellable)
throws Error {
Gee.List<Attachment> list = new Gee.LinkedList<Attachment>();
foreach (RFC822.Part part in attachments) {
Attachment attachment = new Attachment.from_part(message_id, part);
attachment.save(cx, part, attachments_path, cancellable);
list.add(attachment);
}
return list;
}
internal static void delete_attachments(Db.Connection cx,
GLib.File attachments_path,
int64 message_id,
Cancellable? cancellable = null)
throws Error {
Gee.List<Attachment>? attachments = list_attachments(
cx, attachments_path, message_id, cancellable
);
foreach (Attachment attachment in attachments) {
attachment.delete(cx, cancellable);
}
// Ensure they're dead, Jim.
Db.Statement stmt = new Db.Statement(cx, """
DELETE FROM MessageAttachmentTable WHERE message_id = ?
""");
stmt.bind_rowid(0, message_id);
stmt.exec();
}
// XXX this really should be a member of some internal
// ImapDB.Email class.
internal static void add_attachments(Db.Connection cx,
GLib.File attachments_path,
Geary.Email email,
int64 message_id,
Cancellable? cancellable = null)
throws Error {
if (email.fields.fulfills(ImapDB.Attachment.REQUIRED_FIELDS)) {
email.add_attachments(
list_attachments(
cx, attachments_path, message_id, cancellable
)
);
}
}
internal static Gee.List<Attachment> list_attachments(Db.Connection cx,
GLib.File attachments_path,
int64 message_id,
Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare("""
SELECT *
FROM MessageAttachmentTable
WHERE message_id = ?
ORDER BY id
""");
stmt.bind_rowid(0, message_id);
Db.Result results = stmt.exec(cancellable);
Gee.List<Attachment> list = new Gee.LinkedList<Attachment>();
while (!results.finished) {
list.add(new ImapDB.Attachment.from_row(results, attachments_path));
results.next(cancellable);
}
return list;
}
}