Closes #3809. Attachments are now available through the message viewer and are saved as individual files outside of the database.

This commit is contained in:
Nate Lillich 2012-06-08 12:39:27 -07:00
parent 6fcf4cf6ce
commit 0258a20ad9
15 changed files with 842 additions and 239 deletions

15
sql/Version-002.sql Normal file
View file

@ -0,0 +1,15 @@
--
-- MessageAttachmentTable
--
CREATE TABLE MessageAttachmentTable (
id INTEGER PRIMARY KEY,
message_id INTEGER REFERENCES MessageTable ON DELETE CASCADE,
filename TEXT,
mime_type TEXT,
filesize INTEGER
);
CREATE INDEX MessageAttachmentTableMessageIDIndex ON MessageAttachmentTable(message_id);

View file

@ -5,6 +5,7 @@ set(COMMON_SRC
common/common-arrays.vala
common/common-async.vala
common/common-date.vala
common/common-files.vala
common/common-intl.vala
common/common-yorba-application.vala
)
@ -12,6 +13,7 @@ common/common-yorba-application.vala
set(ENGINE_SRC
engine/api/geary-account.vala
engine/api/geary-account-information.vala
engine/api/geary-attachment.vala
engine/api/geary-batch-operations.vala
engine/api/geary-composed-email.vala
engine/api/geary-conversation.vala
@ -133,6 +135,8 @@ engine/sqlite/api/sqlite-folder.vala
engine/sqlite/email/sqlite-folder-row.vala
engine/sqlite/email/sqlite-folder-table.vala
engine/sqlite/email/sqlite-mail-database.vala
engine/sqlite/email/sqlite-message-attachment-row.vala
engine/sqlite/email/sqlite-message-attachment-table.vala
engine/sqlite/email/sqlite-message-location-row.vala
engine/sqlite/email/sqlite-message-location-table.vala
engine/sqlite/email/sqlite-message-row.vala

View file

@ -112,6 +112,8 @@ public class GearyController {
main_window.message_viewer.reply_all_message.connect(on_reply_all_message);
main_window.message_viewer.forward_message.connect(on_forward_message);
main_window.message_viewer.mark_message.connect(on_message_viewer_mark_message);
main_window.message_viewer.open_attachment.connect(on_open_attachment);
main_window.message_viewer.save_attachments.connect(on_save_attachments);
main_window.message_list_view.grab_focus();
@ -942,6 +944,54 @@ public class GearyController {
set_busy(false);
}
private void on_open_attachment(Geary.Attachment attachment) {
open_uri("file://" + attachment.filepath);
}
private void on_save_attachments(Gee.List<Geary.Attachment> attachments) {
Gtk.FileChooserAction action = attachments.size == 1
? Gtk.FileChooserAction.SAVE
: Gtk.FileChooserAction.SELECT_FOLDER;
Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(null, main_window, action,
Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.SAVE, Gtk.ResponseType.ACCEPT, null);
dialog.set_filename(attachments[0].filepath);
if (dialog.run() != Gtk.ResponseType.ACCEPT) {
dialog.destroy();
return;
}
// Get the selected location.
string filename = dialog.get_filename();
debug("Saving attachment to: %s", filename);
// Save the attachments.
// TODO Handle attachments with the same name being saved into the same directory.
File destination = File.new_for_path(filename);
if (attachments.size == 1) {
File source = File.new_for_path(attachments[0].filepath);
source.copy_async.begin(destination, FileCopyFlags.OVERWRITE, Priority.DEFAULT, null,
null, on_save_completed);
} else {
foreach (Geary.Attachment attachment in attachments) {
File dest_name = destination.get_child(attachment.filename);
File source = File.new_for_path(attachment.filepath);
debug("Saving %s to %s", source.get_path(), dest_name.get_path());
source.copy_async.begin(dest_name, FileCopyFlags.OVERWRITE, Priority.DEFAULT, null,
null, on_save_completed);
}
}
dialog.destroy();
}
private void on_save_completed(Object? source, AsyncResult result) {
try {
((File) source).copy_async.end(result);
} catch (Error error) {
warning("Failed to copy attachment to destination: %s", error.message);
}
}
// Opens a link in an external browser.
private void open_uri(string _link) {
string link = _link;

View file

@ -14,7 +14,8 @@ public class MessageViewer : WebKit.WebView {
| Geary.Email.Field.DATE
| Geary.Email.Field.FLAGS
| Geary.Email.Field.PREVIEW;
private const int ATTACHMENT_PREVIEW_SIZE = 50;
private const string MESSAGE_CONTAINER_ID = "message_container";
private const string SELECTION_COUNTER_ID = "multiple_messages";
private const string HTML_BODY = """
@ -185,8 +186,9 @@ public class MessageViewer : WebKit.WebView {
background-color: #e8e8e8
}
.email.hide:not(:last-of-type) .body,
.email:not(.hide) .preview,
.email:last-of-type .preview {
.email.hide:not(:last-of-type) > .attachment_container,
.email:not(.hide) .header_container .preview,
.email:last-of-type .header_container .preview {
display: none;
}
.email:not(:last-of-type) .header_container {
@ -219,6 +221,76 @@ public class MessageViewer : WebKit.WebView {
}
.email:not(.attachment) .attachment.icon {
display: none;
}
.email .header_container .attachment.icon {
float: right;
margin-top: 7px;
}
.email > .attachment_container {
background-color: #ddd;
border-radius: 4px;
padding-bottom: 10px;
}
.email > .attachment_container > .top_border {
border-bottom: 1px solid #999;
border-radius: 0 0 4px 4px;
height: 10px;
background-color: white;
margin-bottom: 5px;
box-shadow: 0 3px 5px #c0c0c0;
}
.email > .attachment_container > .attachment {
margin: 10px 10px 0 10px;
padding: 2px;
overflow: hidden;
font-size: 10pt;
cursor: pointer;
border: 1px solid transparent;
border-radius: 5px;
display: inline;
}
.email > .attachment_container > .attachment:hover,
.email > .attachment_container > .attachment:active {
border-color: #999;
background-color: #e8e8e8;
}
.email > .attachment_container > .attachment:active {
padding: 3px 1px 1px 3px;
box-shadow: inset 3px 3px 5px #ccc, inset -1px -1px 3px #ccc;
}
.email > .attachment_container > .attachment .preview {
width: 52px;
height: 52px;
text-align: center;
vertical-align: middle;
}
.email > .attachment_container > .attachment .preview img {
max-width: 50px;
max-height: 50px;
}
.email > .attachment_container > .attachment .preview .thumbnail {
border: 1px solid #999;
box-shadow: 0 0 5px #b8b8b8;
background-size: 16px 16px;
background-position:0 0, 8px 0, 8px -8px, 0px 8px;
}
.email > .attachment_container > .attachment:hover .preview .thumbnail {
background-image:
-webkit-linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%, transparent),
-webkit-linear-gradient(-45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%, transparent),
-webkit-linear-gradient(45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%),
-webkit-linear-gradient(-45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%);
}
.email > .attachment_container > .attachment .info {
vertical-align: middle;
padding-left: 5px;
}
.email > .attachment_container > .attachment .info > :not(.filename) {
color: #666;
}
.header {
overflow: hidden;
}
@ -314,7 +386,8 @@ public class MessageViewer : WebKit.WebView {
width: auto;
padding: 15px;
}
#email_template {
#email_template,
#attachment_template {
display: none;
}
blockquote {
@ -336,12 +409,23 @@ public class MessageViewer : WebKit.WebView {
<div class="unstarred button"><img src="" class="icon" /></div>
<div class="menu button"><img src="" class="icon" /></div>
</div>
<img src="" class="attachment icon" />
<div class="header"></div>
<div class="preview"></div>
</div>
<div class="body"></div>
</div>
</div>
<div id="attachment_template" class="attachment_container">
<div class="top_border"></div>
<table class="attachment"><tr>
<td class="preview"><img src="" /></td>
<td class="info">
<div class="filename"></div>
<div class="filesize"></div>
</td>
</tr></table>
</div>
</body></html>""";
// Fired when the user clicks a link.
@ -362,10 +446,17 @@ public class MessageViewer : WebKit.WebView {
// Fired when the user marks a message.
public signal void mark_message(Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove);
// Fired when the user opens an attachment.
public signal void open_attachment(Geary.Attachment attachment);
// Fired when the user wants to save one or more attachments.
public signal void save_attachments(Gee.List<Geary.Attachment> attachment);
// List of emails in this view.
public Gee.TreeSet<Geary.Email> messages { get; private set; default =
new Gee.TreeSet<Geary.Email>((CompareFunc<Geary.Email>) Geary.Email.compare_date_ascending); }
public Geary.Email? active_email = null;
public Geary.Attachment? active_attachment = null;
// HTML element that contains message DIVs.
private WebKit.DOM.HTMLDivElement container;
@ -415,6 +506,7 @@ public class MessageViewer : WebKit.WebView {
set_icon_src("#email_template .menu .icon", "down");
set_icon_src("#email_template .starred .icon", "starred");
set_icon_src("#email_template .unstarred .icon", "non-starred-grey");
set_icon_src("#email_template .attachment.icon", "mail-attachment");
}
@ -431,7 +523,7 @@ public class MessageViewer : WebKit.WebView {
private void set_icon_src(string selector, string icon_name) {
try {
// Load the icon.
string icon_filename = IconFactory.instance.lookup_icon(icon_name, 16, 0).get_filename();
string icon_filename = IconFactory.instance.lookup_icon(icon_name, 16).get_filename();
uint8[] icon_content;
FileUtils.get_data(icon_filename, out icon_content);
@ -441,15 +533,56 @@ public class MessageViewer : WebKit.WebView {
icon_content, out uncertain_content_type));
// Then set the source to a data url.
WebKit.DOM.HTMLImageElement icon = get_dom_document().query_selector(selector)
WebKit.DOM.HTMLImageElement img = Util.DOM.select(get_dom_document(), selector)
as WebKit.DOM.HTMLImageElement;
icon.set_attribute("src", "data:%s;base64,%s".printf(icon_mimetype,
Base64.encode(icon_content)));
set_data_url(img, icon_mimetype, icon_content);
} catch (Error error) {
warning("Failed to load icon '%s': %s", icon_name, error.message);
}
}
private void set_image_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename,
int maxwidth, int maxheight = -1) {
if( maxheight == -1 ){
maxheight = maxwidth;
}
try {
// If the file is an image, use it. Otherwise get the icon for this mime_type.
uint8[] content;
string content_type = ContentType.from_mime_type(mime_type);
string icon_mime_type = mime_type;
if (mime_type.has_prefix("image/")) {
// Get a thumbnail for the image.
// TODO Generate and save the thumbnail when extracting the attachments rather than
// when showing them in the viewer.
img.get_class_list().add("thumbnail");
Gdk.Pixbuf image = new Gdk.Pixbuf.from_file_at_scale(filename, maxwidth, maxheight,
true);
image.save_to_buffer(out content, "png");
icon_mime_type = "image/png";
} else {
// Load the icon for this mime type.
ThemedIcon icon = ContentType.get_icon(content_type) as ThemedIcon;
string icon_filename = IconFactory.instance.lookup_icon(icon.names[0], maxwidth)
.get_filename();
FileUtils.get_data(icon_filename, out content);
icon_mime_type = ContentType.get_mime_type(ContentType.guess(icon_filename, content,
null));
}
// Then set the source to a data url.
set_data_url(img, icon_mime_type, content);
} catch (Error error) {
warning("Failed to load image '%s': %s", filename, error.message);
}
}
private void set_data_url(WebKit.DOM.HTMLImageElement img, string mime_type, uint8[] content)
throws Error {
img.set_attribute("src", "data:%s;base64,%s".printf(mime_type, Base64.encode(content)));
}
// Removes all displayed e-mails from the view.
public void clear() {
// Remove all messages from DOM.
@ -547,12 +680,10 @@ public class MessageViewer : WebKit.WebView {
// </span>
// </div>
// </div>
div_message = get_dom_document().get_element_by_id("email_template").clone_node(true)
as WebKit.DOM.HTMLElement;
div_message = Util.DOM.clone_select(get_dom_document(), "#email_template");
div_message.set_attribute("id", message_id);
container.insert_before(div_message, insert_before);
div_email_container = div_message.query_selector("div.email_container")
as WebKit.DOM.HTMLElement;
div_email_container = Util.DOM.select(div_message, "div.email_container");
if (email.is_unread() == Geary.Trillian.FALSE) {
div_message.get_class_list().add("hide");
}
@ -598,7 +729,7 @@ public class MessageViewer : WebKit.WebView {
// Add the avatar.
try {
WebKit.DOM.HTMLImageElement icon = get_dom_document().query_selector("#%s .avatar".printf(message_id))
WebKit.DOM.HTMLImageElement icon = Util.DOM.select(div_message, ".avatar")
as WebKit.DOM.HTMLImageElement;
string checksum = GLib.Checksum.compute_for_string (
GLib.ChecksumType.MD5, email.sender.get(0).address);
@ -610,8 +741,8 @@ public class MessageViewer : WebKit.WebView {
// Insert the preview text.
try {
WebKit.DOM.HTMLElement preview = div_message.query_selector(".header_container .preview")
as WebKit.DOM.HTMLElement;
WebKit.DOM.HTMLElement preview =
Util.DOM.select(div_message, ".header_container .preview");
string preview_str = email.get_preview_as_string();
if (preview_str.length == Geary.Email.MAX_PREVIEW_BYTES) {
preview_str += "";
@ -637,28 +768,35 @@ public class MessageViewer : WebKit.WebView {
// Graft header and email body into the email container.
try {
WebKit.DOM.HTMLElement table_header = div_email_container
.query_selector(".header_container .header") as WebKit.DOM.HTMLElement;
WebKit.DOM.HTMLElement table_header =
Util.DOM.select(div_email_container, ".header_container .header");
table_header.set_inner_html(header);
WebKit.DOM.HTMLElement span_body = div_email_container.query_selector(".body")
as WebKit.DOM.HTMLElement;
WebKit.DOM.HTMLElement span_body = Util.DOM.select(div_email_container, ".body");
span_body.set_inner_html(body_text);
} catch (Error html_error) {
warning("Error setting HTML for message: %s", html_error.message);
}
// Add the attachments container if we have any attachments.
if (email.attachments.size > 0) {
insert_attachments(div_message, email.attachments);
}
// Add classes according to the state of the email.
update_flags(email);
// Attach to the click events for hiding/showing quotes, opening the menu, and so forth.
bind_event(this, ".email", "contextmenu", (Callback) on_context_menu, this);
bind_event(this,".quote_container > .hider", "click", (Callback) on_hide_quote_clicked);
bind_event(this,".quote_container > .shower", "click", (Callback) on_show_quote_clicked);
bind_event(this,".email_container .menu", "click", (Callback) on_menu_clicked, this);
bind_event(this,".email_container .starred", "click", (Callback) on_unstar_clicked, this);
bind_event(this,".email_container .unstarred", "click", (Callback) on_star_clicked, this);
bind_event(this,".email .header_container", "click", (Callback) on_body_toggle_clicked, this);
bind_event(this, ".quote_container > .hider", "click", (Callback) on_hide_quote_clicked);
bind_event(this, ".quote_container > .shower", "click", (Callback) on_show_quote_clicked);
bind_event(this, ".email_container .menu", "click", (Callback) on_menu_clicked, this);
bind_event(this, ".email_container .starred", "click", (Callback) on_unstar_clicked, this);
bind_event(this, ".email_container .unstarred", "click", (Callback) on_star_clicked, this);
bind_event(this, ".email .header_container", "click", (Callback) on_body_toggle_clicked, this);
bind_event(this, ".attachment_container .attachment", "click", (Callback) on_attachment_clicked, this);
bind_event(this, ".attachment_container .attachment", "contextmenu", (Callback) on_attachment_menu, this);
}
private WebKit.DOM.HTMLElement? closest_ancestor(WebKit.DOM.Element element, string selector) {
@ -714,7 +852,7 @@ public class MessageViewer : WebKit.WebView {
Geary.EmailFlags flags = email.email_flags;
// Update the flags in out message set.
// Update the flags in our message set.
foreach (Geary.Email message in messages) {
if (message.id.equals(email.id)) {
message.set_flags(flags);
@ -726,21 +864,14 @@ public class MessageViewer : WebKit.WebView {
WebKit.DOM.HTMLElement container = email_to_element.get(email.id);
try {
WebKit.DOM.DOMTokenList class_list = container.get_class_list();
if (flags.is_unread()) {
class_list.remove("read");
} else {
class_list.add("read");
}
if (flags.is_flagged()) {
class_list.add("starred");
} else {
class_list.remove("starred");
}
Util.DOM.toggle_class(class_list, "read", !flags.is_unread());
Util.DOM.toggle_class(class_list, "starred", flags.is_flagged());
Util.DOM.toggle_class(class_list, "attachment", email.attachments.size > 0);
} catch (Error e) {
warning("Failed to set classes on .email: %s", e.message);
}
}
private static void on_context_menu(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
message_viewer.active_email = message_viewer.get_email_from_element(element);
@ -768,10 +899,10 @@ public class MessageViewer : WebKit.WebView {
private static void on_menu_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
event.stop_propagation();
message_viewer.on_menu_clicked_async(element);
message_viewer.on_menu_clicked_self(element);
}
private void on_menu_clicked_async(WebKit.DOM.Element element) {
private void on_menu_clicked_self(WebKit.DOM.Element element) {
active_email = get_email_from_element(element);
show_message_menu(element);
}
@ -779,10 +910,10 @@ public class MessageViewer : WebKit.WebView {
private static void on_unstar_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
event.stop_propagation();
message_viewer.on_unstar_clicked_async(element);
message_viewer.on_unstar_clicked_self(element);
}
private void on_unstar_clicked_async(WebKit.DOM.Element element){
private void on_unstar_clicked_self(WebKit.DOM.Element element){
active_email = get_email_from_element(element);
on_unflag_message();
}
@ -790,10 +921,10 @@ public class MessageViewer : WebKit.WebView {
private static void on_star_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
event.stop_propagation();
message_viewer.on_star_clicked_async(element);
message_viewer.on_star_clicked_self(element);
}
private void on_star_clicked_async(WebKit.DOM.Element element){
private void on_star_clicked_self(WebKit.DOM.Element element){
active_email = get_email_from_element(element);
on_flag_message();
}
@ -815,15 +946,53 @@ public class MessageViewer : WebKit.WebView {
class_list.add("hide");
}
} catch (Error error) {
warning("Error toggline message: %s", error.message);
warning("Error toggling message: %s", error.message);
}
}
private static void on_attachment_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
message_viewer.on_attachment_clicked_self(element);
}
private void on_attachment_clicked_self(WebKit.DOM.Element element) {
try {
int64 attachment_id = int64.parse(element.get_attribute("data-attachment-id"));
open_attachment(get_email_from_element(element).get_attachment(attachment_id));
} catch (Error error) {
warning("Error opening attachment: %s", error.message);
}
}
private static void on_attachment_menu(WebKit.DOM.Element element, WebKit.DOM.Event event,
MessageViewer message_viewer) {
try {
event.stop_propagation();
message_viewer.active_email = message_viewer.get_email_from_element(element);
message_viewer.active_attachment = message_viewer.active_email.get_attachment(
int64.parse(element.get_attribute("data-attachment-id")));
message_viewer.show_message_menu(element);
} catch (Error error) {
warning("Error opening attachment menu: %s", error.message);
}
}
private void on_message_menu_selection_done() {
active_email = null;
active_attachment = null;
message_menu = null;
}
private void on_save_attachment() {
Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
attachments.add(active_attachment != null ? active_attachment : active_email.attachments[0]);
save_attachments(attachments);
}
private void on_save_all_attachments() {
save_attachments(active_email.attachments);
}
private void on_reply_to_message() {
reply_to_message();
}
@ -874,6 +1043,29 @@ public class MessageViewer : WebKit.WebView {
message_menu = new Gtk.Menu();
message_menu.selection_done.connect(on_message_menu_selection_done);
if (active_email.attachments.size > 0) {
// Save attachment as...
if (active_attachment != null) {
Gtk.MenuItem save_attachment_item = new Gtk.MenuItem.with_mnemonic(_("_Save As..."));
save_attachment_item.activate.connect(on_save_attachment);
message_menu.append(save_attachment_item);
}
// Save all attachments
if (active_email.attachments.size > 1) {
Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
save_all_item.activate.connect(on_save_all_attachments);
message_menu.append(save_all_item);
} else if (active_attachment == null) {
Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save A_ttachment..."));
save_all_item.activate.connect(on_save_attachment);
message_menu.append(save_all_item);
}
// Separator.
message_menu.append(new Gtk.SeparatorMenuItem());
}
// Reply to a message.
Gtk.MenuItem reply_item = new Gtk.MenuItem.with_mnemonic(_("_Reply"));
reply_item.activate.connect(on_reply_to_message);
@ -1002,7 +1194,7 @@ public class MessageViewer : WebKit.WebView {
// Copy the stuff before the quote, then the wrapped quote.
WebKit.DOM.Element quote_container = create_quote_container();
((WebKit.DOM.HTMLElement) quote_container.query_selector(".quote")).set_inner_html(
Util.DOM.select(quote_container, ".quote").set_inner_html(
text.substring(quote_start, quote_end - quote_start));
container.append_child(quote_container);
if (quote_start > offset) {
@ -1042,7 +1234,7 @@ public class MessageViewer : WebKit.WebView {
// Some HTML messages like to wrap themselves in full, proper html, head, and body tags.
// If we have that here, lets remove it since we are sticking it in our own document.
WebKit.DOM.HTMLElement? body = container.query_selector("body") as WebKit.DOM.HTMLElement;
WebKit.DOM.HTMLElement? body = Util.DOM.select(container, "body");
if (body != null) {
container.set_inner_html(body.get_inner_html());
}
@ -1065,7 +1257,7 @@ public class MessageViewer : WebKit.WebView {
// blockquote
// sibling
WebKit.DOM.Element quote_container = create_quote_container();
quote_container.query_selector(".quote").append_child(blockquote_node);
Util.DOM.select(quote_container, ".quote").append_child(blockquote_node);
if (next_sibling == null) {
parent.append_child(quote_container);
} else {
@ -1217,7 +1409,59 @@ public class MessageViewer : WebKit.WebView {
output = r.replace_eval(output, -1, 0, 0, is_valid_url);
return output.replace(" \01 ", "&lt;").replace(" \02 ", "&gt;");
}
private void insert_attachments(WebKit.DOM.HTMLElement email_container,
Gee.List<Geary.Attachment> attachments) {
// <div class="attachment_container">
// <div class="top_border"></div>
// <table class="attachment" data-attachment-id="">
// <tr>
// <td class="preview">
// <img src="" />
// </td>
// <td class="info">
// <div class="filename"></div>
// <div class="filesize"></div>
// </td>
// </tr>
// </table>
// </div>
try {
// Prepare the dom for our attachments.
WebKit.DOM.Document document = get_dom_document();
WebKit.DOM.HTMLElement attachment_container =
Util.DOM.clone_select(document, "#attachment_template");
WebKit.DOM.HTMLElement attachment_template =
Util.DOM.select(attachment_container, ".attachment");
attachment_container.remove_attribute("id");
attachment_container.remove_child(attachment_template);
// Create an attachment table for each attachment.
foreach (Geary.Attachment attachment in attachments) {
// Generate the attachment table.
WebKit.DOM.HTMLElement attachment_table = Util.DOM.clone_node(attachment_template);
Util.DOM.select(attachment_table, ".info .filename")
.set_inner_text(attachment.filename);
Util.DOM.select(attachment_table, ".info .filesize")
.set_inner_text(Files.get_filesize_as_string(attachment.filesize));
attachment_table.set_attribute("data-attachment-id", "%lld".printf(attachment.id));
// Set the image preview and insert it into the container.
WebKit.DOM.HTMLImageElement img =
Util.DOM.select(attachment_table, ".preview img") as WebKit.DOM.HTMLImageElement;
set_image_src(img, attachment.mime_type, attachment.filepath, ATTACHMENT_PREVIEW_SIZE);
attachment_container.append_child(attachment_table);
}
// Append the attachments to the email.
email_container.append_child(attachment_container);
} catch (Error error) {
debug("Failed to insert attachments: %s", error.message);
}
}
// Validates a URL.
// Ensures the URL begins with a valid protocol specifier. (If not, we don't
// want to linkify it.)

View file

@ -11,6 +11,39 @@ public const string URL_REGEX = "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|ww
// Regex to determine if a URL has a known protocol.
public const string PROTOCOL_REGEX = "^(aim|apt|bitcoin|cvs|ed2k|ftp|file|finger|git|gtalk|http|https|irc|ircs|irc6|lastfm|ldap|ldaps|magnet|news|nntp|rsync|sftp|skype|smb|sms|svn|telnet|tftp|ssh|webcal|xmpp):";
// TODO Move these other functions and variables into this namespace.
namespace Util.DOM {
public WebKit.DOM.HTMLElement? select(WebKit.DOM.Node node, string selector) {
try {
if (node is WebKit.DOM.Document) {
return (node as WebKit.DOM.Document).query_selector(selector) as WebKit.DOM.HTMLElement;
} else {
return (node as WebKit.DOM.Element).query_selector(selector) as WebKit.DOM.HTMLElement;
}
} catch (Error error) {
debug("Error selecting element %s: %s", selector, error.message);
return null;
}
}
public WebKit.DOM.HTMLElement? clone_node(WebKit.DOM.Node node, bool deep = true) {
return node.clone_node(deep) as WebKit.DOM.HTMLElement;
}
public WebKit.DOM.HTMLElement? clone_select(WebKit.DOM.Node node, string selector,
bool deep = true) {
return clone_node(select(node, selector), deep);
}
public void toggle_class(WebKit.DOM.DOMTokenList class_list, string clas, bool add) throws Error {
if (add) {
class_list.add(clas);
} else {
class_list.remove(clas);
}
}
}
public void bind_event(WebKit.WebView view, string selector, string event, Callback callback,
Object? extra = null) {
try {

View file

@ -0,0 +1,39 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
namespace Files {
public const int64 KILOBYTE = 1024;
public const int64 MEGABYTE = KILOBYTE * 1024;
public const int64 GIGABYTE = MEGABYTE * 1024;
public const int64 TERABYTE = GIGABYTE * 1024;
public string get_filesize_as_string(int64 filesize) {
int64 scale = 1;
string units = _("bytes");
if (filesize > TERABYTE) {
scale = TERABYTE;
units = C_("Abbreviation for terabyte", "TB");
} else if (filesize > GIGABYTE) {
scale = GIGABYTE;
units = C_("Abbreviation for gigabyte", "GB");
} else if (filesize > MEGABYTE) {
scale = MEGABYTE;
units = C_("Abbreviation for megabyte", "MB");
} else if (filesize > KILOBYTE) {
scale = KILOBYTE;
units = C_("Abbreviation for kilobyte", "KB");
}
if (scale == 1) {
return "%lld %s".printf(filesize, units);
} else {
return "%.2f %s".printf((float) filesize / (float) scale, units);
}
}
}

View file

@ -0,0 +1,32 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Attachment {
public const Email.Field REQUIRED_FIELDS = Email.Field.HEADER | Email.Field.BODY;
public string filename { get; private set; }
public string filepath { get; private set; }
public string mime_type { get; private set; }
public int64 filesize { get; private set; }
public int64 id { get; private set; }
internal Attachment(File data_dir, string filename, string mime_type, int64 filesize,
int64 message_id, int64 attachment_id) {
this.filename = filename;
this.mime_type = mime_type;
this.filesize = filesize;
this.filepath = get_path(data_dir, message_id, attachment_id, filename);
this.id = attachment_id;
}
internal static string get_path(File data_dir, int64 message_id, int64 attachment_id,
string filename) {
return "%s/attachments/%lld/%lld/%s".printf(data_dir.get_path(), message_id, attachment_id,
filename);
}
}

View file

@ -113,6 +113,8 @@ public class Geary.Email : Object {
// BODY
public RFC822.Text? body { get; private set; default = null; }
public Gee.List<Geary.Attachment> attachments { get; private set;
default = new Gee.ArrayList<Geary.Attachment>(); }
// PROPERTIES
public Geary.EmailProperties? properties { get; private set; default = null; }
@ -216,13 +218,17 @@ public class Geary.Email : Object {
fields |= Field.PREVIEW;
}
public void set_flags(Geary.EmailFlags email_flags) {
this.email_flags = email_flags;
fields |= Field.FLAGS;
}
public void add_attachment(Geary.Attachment attachment) {
attachments.add(attachment);
}
/**
* This method requires Geary.Email.Field.HEADER and Geary.Email.Field.BODY be present.
* If not, EngineError.INCOMPLETE_MESSAGE is thrown.
@ -238,7 +244,19 @@ public class Geary.Email : Object {
return message;
}
public Geary.Attachment? get_attachment(int64 attachment_id) throws EngineError, RFC822Error {
if (!fields.fulfills(Field.HEADER | Field.BODY))
throw new EngineError.INCOMPLETE_MESSAGE("Parsed email requires HEADER and BODY");
foreach (Geary.Attachment attachment in attachments) {
if (attachment.id == attachment_id) {
return attachment;
}
}
return null;
}
/**
* Returns a list of this email's ancestry by Message-ID. IDs are not returned in any
* particular order. The ancestry is made up from this email's Message-ID, its References,

View file

@ -265,28 +265,9 @@ public class Geary.RFC822.Message : Object {
}
// convert payload to a buffer
GMime.DataWrapper? wrapper = part.get_content_object();
if (wrapper == null) {
throw new RFC822Error.INVALID("Could not get the content wrapper for content-type %s",
content_type);
}
ByteArray byte_array = new ByteArray();
GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array);
stream.set_owner(false);
// Convert encoding to UTF-8.
GMime.StreamFilter stream_filter = new GMime.StreamFilter(stream);
string? charset = part.get_content_type_parameter("charset");
if (charset == null)
charset = DEFAULT_ENCODING;
stream_filter.add(new GMime.FilterCharset(charset, "UTF8"));
wrapper.write_to_stream(stream_filter);
return new Geary.Memory.Buffer(byte_array.data, byte_array.len);
return mime_part_to_memory_buffer(part, true);
}
private GMime.Part? find_first_mime_part(GMime.Object current_root, string content_type) {
// descend looking for the content type in a GMime.Part
GMime.Multipart? multipart = current_root as GMime.Multipart;
@ -298,14 +279,68 @@ public class Geary.RFC822.Message : Object {
return child_part;
}
}
GMime.Part? part = current_root as GMime.Part;
if (part != null && part.get_content_type().to_string() == content_type)
if (part != null && part.get_content_type().to_string() == content_type &&
part.get_disposition() != "attachment") {
return part;
}
return null;
}
internal Gee.List<GMime.Part> get_attachments() throws RFC822Error {
Gee.List<GMime.Part> attachments = new Gee.ArrayList<GMime.Part>();
find_attachments( ref attachments, message.get_mime_part() );
return attachments;
}
private void find_attachments(ref Gee.List<GMime.Part> attachments, GMime.Object root)
throws RFC822Error {
// If this is a multipart container, dive into each of its children.
if (root is GMime.Multipart) {
GMime.Multipart multipart = root as GMime.Multipart;
int count = multipart.get_count();
for (int i = 0; i < count; ++i) {
find_attachments(ref attachments, multipart.get_part(i));
}
return;
}
// Otherwise see if it has a content disposition of "attachment."
if (root is GMime.Part && root.get_disposition() == "attachment") {
attachments.add(root as GMime.Part);
}
}
private Geary.Memory.AbstractBuffer mime_part_to_memory_buffer(GMime.Part part,
bool to_utf8 = false) throws RFC822Error {
GMime.DataWrapper? wrapper = part.get_content_object();
if (wrapper == null) {
throw new RFC822Error.INVALID("Could not get the content wrapper for content-type %s",
part.get_content_type().to_string());
}
ByteArray byte_array = new ByteArray();
GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array);
stream.set_owner(false);
// Convert encoding to UTF-8.
GMime.StreamFilter stream_filter = new GMime.StreamFilter(stream);
if (to_utf8) {
string? charset = part.get_content_type_parameter("charset");
if (charset == null)
charset = DEFAULT_ENCODING;
stream_filter.add(new GMime.FilterCharset(charset, "UTF8"));
}
wrapper.write_to_stream(stream_filter);
return new Geary.Memory.Buffer(byte_array.data, byte_array.len);
}
public string to_string() {
return message.to_string();
}

View file

@ -6,6 +6,7 @@
public abstract class Geary.Sqlite.Database {
internal SQLHeavy.VersionedDatabase db;
internal File data_dir;
internal File schema_dir;
private Gee.HashMap<SQLHeavy.Table, Geary.Sqlite.Table> table_map = new Gee.HashMap<
@ -17,8 +18,9 @@ public abstract class Geary.Sqlite.Database {
public Database(File db_file, File schema_dir) throws Error {
this.schema_dir = schema_dir;
if (!db_file.get_parent().query_exists())
db_file.get_parent().make_directory_with_parents();
data_dir = db_file.get_parent();
if (!data_dir.query_exists())
data_dir.make_directory_with_parents();
db = new SQLHeavy.VersionedDatabase(db_file.get_path(), schema_dir.get_path());
db.foreign_keys = true;

View file

@ -35,6 +35,7 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
private Geary.Imap.FolderProperties? properties;
private MessageTable message_table;
private MessageLocationTable location_table;
private MessageAttachmentTable attachment_table;
private ImapMessagePropertiesTable imap_message_properties_table;
private Geary.FolderPath path;
@ -47,6 +48,7 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
message_table = db.get_message_table();
location_table = db.get_message_location_table();
attachment_table = db.get_message_attachment_table();
imap_message_properties_table = db.get_imap_message_properties_table();
}
@ -232,7 +234,13 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID,
message_id, folder_row.id, email.id.ordering, email.position);
yield location_table.create_async(transaction, location_row, cancellable);
// Also add attachments if we have them.
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS)) {
Gee.List<GMime.Part> attachments = email.get_message().get_attachments();
yield save_attachments_async(transaction, attachments, message_id, cancellable);
}
// only write out the IMAP email properties if they're supplied and there's something to
// write out -- no need to create an empty row
Geary.Imap.EmailProperties? properties = (Geary.Imap.EmailProperties?) email.properties;
@ -251,7 +259,7 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
return true;
}
public async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
check_open();
@ -325,76 +333,22 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
if (list == null || list.size == 0)
return null;
bool partial_ok = flags.is_all_set(ListFlags.PARTIAL_OK);
bool include_removed = flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE);
// TODO: As this loop involves multiple database operations to form an email, might make
// sense in the future to launch each async method separately, putting the final results
// together when all the information is fetched
Gee.List<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
foreach (MessageLocationRow location_row in list) {
// PROPERTIES and FLAGS are held in separate table from messages, pull from MessageTable
// only if something is needed from there
Geary.Email.Field message_fields =
required_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS);
// fetch the message itself
MessageRow? message_row = null;
if (message_fields != Geary.Email.Field.NONE) {
message_row = yield message_table.fetch_async(transaction, location_row.message_id,
message_fields, cancellable);
assert(message_row != null);
// only add to the list if the email contains all the required fields (because
// properties comes out of a separate table, skip this if properties are requested)
if (!partial_ok && !message_row.fields.fulfills(message_fields))
continue;
}
ImapMessagePropertiesRow? properties = null;
if (required_fields.is_any_set(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS)) {
properties = yield imap_message_properties_table.fetch_async(transaction,
location_row.message_id, cancellable);
if (!partial_ok && properties == null)
continue;
}
Geary.Imap.UID uid = new Geary.Imap.UID(location_row.ordering);
int position = yield location_row.get_position_async(transaction, include_removed,
cancellable);
if (position == -1) {
debug("WARNING: Unable to locate position of email during list of %s, dropping",
to_string());
continue;
}
Geary.Imap.EmailIdentifier email_id = new Geary.Imap.EmailIdentifier(uid);
Geary.Email email = (message_row != null)
? message_row.to_email(position, email_id)
: new Geary.Email(position, email_id);
if (properties != null) {
if (required_fields.require(Geary.Email.Field.PROPERTIES)) {
Imap.EmailProperties? email_properties = properties.get_imap_email_properties();
if (email_properties != null)
email.set_email_properties(email_properties);
else if (!partial_ok)
continue;
}
if (required_fields.require(Geary.Email.Field.FLAGS)) {
EmailFlags? email_flags = properties.get_email_flags();
if (email_flags != null)
email.set_flags(email_flags);
else if (!partial_ok)
continue;
try {
emails.add(yield location_to_email_async(transaction, location_row, required_fields,
flags, cancellable));
} catch (EngineError error) {
if (error is EngineError.NOT_FOUND) {
debug("WARNING: Message not found, dropping: %s", error.message);
} else if (!(error is EngineError.INCOMPLETE_MESSAGE)) {
throw error;
}
}
emails.add(email);
}
return (emails.size > 0) ? emails : null;
@ -405,8 +359,6 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
check_open();
Geary.Imap.UID uid = ((Imap.EmailIdentifier) id).uid;
bool partial_ok = flags.is_all_set(ListFlags.PARTIAL_OK);
bool include_removed = flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE);
Transaction transaction = yield db.begin_transaction_async("Folder.fetch_email_async",
cancellable);
@ -417,37 +369,55 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(),
to_string());
}
return yield location_to_email_async(transaction, location_row, required_fields, flags,
cancellable);
}
public async Geary.Email location_to_email_async(Transaction transaction,
MessageLocationRow location_row, Geary.Email.Field required_fields, ListFlags flags,
Cancellable? cancellable = null) throws Error {
// Prepare our IDs and flags.
Geary.Imap.UID uid = new Geary.Imap.UID(location_row.ordering);
Geary.Imap.EmailIdentifier id = new Geary.Imap.EmailIdentifier(uid);
bool partial_ok = flags.is_all_set(ListFlags.PARTIAL_OK);
bool include_removed = flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE);
// PROPERTIES and FLAGS are held in separate table from messages, pull from MessageTable
// only if something is needed from there
Geary.Email.Field message_fields =
required_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS);
int position = yield location_row.get_position_async(transaction, include_removed, cancellable);
if (position == -1) {
throw new EngineError.NOT_FOUND("Unable to determine position of email %s in %s",
id.to_string(), to_string());
}
// loopback on perverse case
if (required_fields == Geary.Email.Field.NONE)
return new Geary.Email(position, id);
// Only fetch message row if we have fields other than Properties and Flags
MessageRow? message_row = null;
if (required_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS) != 0) {
message_row = yield message_table.fetch_async(transaction,
location_row.message_id, required_fields, cancellable);
if (message_fields != Geary.Email.Field.NONE) {
message_row = yield message_table.fetch_async(transaction, location_row.message_id,
message_fields, cancellable);
if (message_row == null) {
throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(),
to_string());
}
// see if the message row fulfills everything but properties, which are held in
// separate table
if (!partial_ok &&
!message_row.fields.fulfills(required_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS))) {
if (!partial_ok && !message_row.fields.fulfills(message_fields)) {
throw new EngineError.INCOMPLETE_MESSAGE(
"Message %s in folder %s only fulfills %Xh fields (required: %Xh)", id.to_string(),
to_string(), message_row.fields, required_fields);
"Message %s in folder %s only fulfills %Xh fields (required: %Xh)",
id.to_string(), to_string(), message_row.fields, required_fields);
}
}
ImapMessagePropertiesRow? properties = null;
if (required_fields.is_any_set(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS)) {
properties = yield imap_message_properties_table.fetch_async(transaction,
@ -458,19 +428,20 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
id.to_string(), to_string());
}
}
Geary.Email email;
email = message_row != null ? message_row.to_email(position, id) : email =
new Geary.Email(position, id);
Geary.Email email = message_row != null
? message_row.to_email(position, id)
: new Geary.Email(position, id);
if (properties != null) {
if (required_fields.require(Geary.Email.Field.PROPERTIES)) {
Imap.EmailProperties? email_properties = properties.get_imap_email_properties();
if (email_properties != null) {
email.set_email_properties(email_properties);
} else if (!partial_ok) {
throw new EngineError.INCOMPLETE_MESSAGE("Message %s in folder %s does not have PROPERTIES fields",
id.to_string(), to_string());
throw new EngineError.INCOMPLETE_MESSAGE(
"Message %s in folder %s does not have PROPERTIES fields", id.to_string(),
to_string());
}
}
@ -479,15 +450,26 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
if (email_flags != null) {
email.set_flags(email_flags);
} else if (!partial_ok) {
throw new EngineError.INCOMPLETE_MESSAGE("Message %s in folder %s does not have FLAGS fields",
id.to_string(), to_string());
throw new EngineError.INCOMPLETE_MESSAGE(
"Message %s in folder %s does not have FLAGS fields", id.to_string(),
to_string());
}
}
}
// Load the attachments as well if we have the full message.
if (required_fields.fulfills(Geary.Attachment.REQUIRED_FIELDS)) {
Gee.List<MessageAttachmentRow> attachments = yield attachment_table.list_async(
transaction, location_row.message_id, cancellable);
foreach (MessageAttachmentRow row in attachments) {
email.add_attachment(row.to_attachment());
}
}
return email;
}
public async Geary.Imap.UID? get_earliest_uid_async(Cancellable? cancellable = null) throws Error {
return yield get_uid_extremes_async(true, cancellable);
}
@ -624,20 +606,34 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
// if nothing to merge, nothing to do
if (email.fields == Geary.Email.Field.NONE)
return;
// Only merge with MessageTable if has fields applicable to it
if (email.fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS) != 0) {
MessageRow? message_row = yield message_table.fetch_async(transaction, message_id, email.fields,
cancellable);
MessageRow? message_row = yield message_table.fetch_async(transaction, message_id,
(email.fields | Attachment.REQUIRED_FIELDS), cancellable);
assert(message_row != null);
Geary.Email.Field db_fields = message_row.fields;
message_row.merge_from_remote(email);
// possible nothing has changed or been added
if (message_row.fields != Geary.Email.Field.NONE)
// Get the combined email from the merge which will be used below to save the attachments.
Geary.Email combined_email = message_row.to_email(email.position, email.id);
// Next see if all the fields we've received are already in the DB. If they are then
// there is nothing for us to do.
if ((db_fields & email.fields) != email.fields) {
yield message_table.merge_async(transaction, message_row, cancellable);
// Also update the saved attachments if we don't already have them in the database
// and between the database and the new fields we have what is required.
if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS) &&
combined_email.fields.fulfills(Attachment.REQUIRED_FIELDS)) {
yield save_attachments_async(transaction,
combined_email.get_message().get_attachments(), message_id, cancellable);
}
}
}
// update IMAP properties
if (email.fields.fulfills(Geary.Email.Field.PROPERTIES)) {
Geary.Imap.EmailProperties properties = (Geary.Imap.EmailProperties) email.properties;
@ -658,7 +654,63 @@ private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics {
flags, cancellable);
}
}
private async void save_attachments_async(Transaction transaction,
Gee.List<GMime.Part> attachments, int64 message_id, Cancellable? cancellable = null)
throws Error {
// Nothing to do if no attachments.
if (attachments.size == 0){
return;
}
foreach (GMime.Part attachment in attachments) {
// Get the info about the attachment.
string? filename = attachment.get_filename();
string mime_type = attachment.get_content_type().to_string();
if (filename == null || filename.length == 0) {
filename = _("none");
}
// Convert the attachment content into a usable ByteArray.
GMime.DataWrapper attachment_data = attachment.get_content_object();
ByteArray byte_array = new ByteArray();
GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array);
stream.set_owner(false);
attachment_data.write_to_stream(stream);
uint filesize = byte_array.len;
// Insert it into the database.
MessageAttachmentRow attachment_row = new MessageAttachmentRow(attachment_table, 0,
message_id, filename, mime_type, filesize);
int64 attachment_id = yield attachment_table.create_async(transaction, attachment_row,
cancellable);
try {
// Create the file where the attachment will be saved and get the output stream.
string saved_name = Attachment.get_path(db.data_dir, message_id, attachment_id,
filename);
debug("Saving attachment to %s", saved_name);
File saved_file = File.new_for_path(saved_name);
saved_file.get_parent().make_directory_with_parents();
FileOutputStream saved_stream = yield saved_file.create_async(
FileCreateFlags.REPLACE_DESTINATION, Priority.DEFAULT, cancellable);
// Save the data to disk and flush it.
yield saved_stream.write_async(byte_array.data[0:filesize], Priority.DEFAULT,
cancellable);
yield saved_stream.flush_async();
} catch (Error error) {
// An error occurred while saving the attachment, so lets remove the attachment from
// the database.
// TODO Use SQLite transactions here and do a rollback.
debug("Failed to save attachment: %s", error.message);
yield attachment_table.remove_async(transaction, attachment_id, cancellable);
throw error;
}
}
}
public async void remove_marked_email_async(Geary.EmailIdentifier id, out bool marked,
Cancellable? cancellable) throws Error {
check_open();

View file

@ -6,10 +6,9 @@
public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
public const string FILENAME = "geary.db";
public MailDatabase(string user, File user_data_dir, File resource_dir) throws Error {
base (user_data_dir.get_child(user).get_child(FILENAME),
resource_dir.get_child("sql"));
base (user_data_dir.get_child(user).get_child(FILENAME), resource_dir.get_child("sql"));
}
public Geary.Sqlite.FolderTable get_folder_table() {
@ -39,5 +38,15 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
? location_table
: (MessageLocationTable) add_table(new MessageLocationTable(this, heavy_table));
}
public Geary.Sqlite.MessageAttachmentTable get_message_attachment_table() {
SQLHeavy.Table heavy_table;
MessageAttachmentTable? attachment_table = get_table("MessageAttachmentTable", out heavy_table)
as MessageAttachmentTable;
return (attachment_table != null)
? attachment_table
: (MessageAttachmentTable) add_table(new MessageAttachmentTable(this, heavy_table));
}
}

View file

@ -0,0 +1,39 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.MessageAttachmentRow : Geary.Sqlite.Row {
public int64 id { get; private set; }
public int64 message_id { get; private set; }
public int64 filesize { get; private set; }
public string filename { get; private set; }
public string mime_type { get; private set; }
public MessageAttachmentRow(MessageAttachmentTable table, int64 id, int64 message_id,
string filename, string mime_type, int64 filesize) {
base (table);
this.id = id;
this.message_id = message_id;
this.filename = filename;
this.mime_type = mime_type;
this.filesize = filesize;
}
public MessageAttachmentRow.from_query_result(MessageAttachmentTable table,
SQLHeavy.QueryResult result) throws Error {
base (table);
id = fetch_int64_for(result, MessageAttachmentTable.Column.ID);
message_id = fetch_int64_for(result, MessageAttachmentTable.Column.MESSAGE_ID);
filename = fetch_string_for(result, MessageAttachmentTable.Column.FILENAME);
mime_type = fetch_string_for(result, MessageAttachmentTable.Column.MIME_TYPE);
}
public Geary.Attachment to_attachment() {
return new Attachment(table.gdb.data_dir, filename, mime_type, filesize, message_id, id);
}
}

View file

@ -0,0 +1,90 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.MessageAttachmentTable : Geary.Sqlite.Table {
// This row *must* match the order in the schema
public enum Column {
ID,
MESSAGE_ID,
FILENAME,
MIME_TYPE,
FILESIZE
}
public MessageAttachmentTable(Geary.Sqlite.Database db, SQLHeavy.Table table) {
base (db, table);
}
public async int64 create_async(Transaction? transaction, MessageAttachmentRow row,
Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction, "MessageAttachmentTable.create_async",
cancellable);
SQLHeavy.Query query = locked.prepare(
"INSERT INTO MessageAttachmentTable (message_id, filename, mime_type, filesize) " +
"VALUES (?, ?, ?, ?)");
query.bind_int64(0, row.message_id);
query.bind_string(1, row.filename);
query.bind_string(2, row.mime_type);
query.bind_int64(3, row.filesize);
int64 id = yield query.execute_insert_async(cancellable);
locked.set_commit_required();
yield release_lock_async(transaction, locked, cancellable);
check_cancel(cancellable, "create_async");
return id;
}
public async Gee.List<MessageAttachmentRow>? list_async(Transaction? transaction,
int64 message_id, Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction, "MessageAttachmentTable.list_async",
cancellable);
SQLHeavy.Query query = locked.prepare(
"SELECT id, filename, mime_type, filesize FROM MessageAttachmentTable " +
"WHERE message_id = ? ORDER BY id");
query.bind_int64(0, message_id);
SQLHeavy.QueryResult results = yield query.execute_async();
check_cancel(cancellable, "list_async");
Gee.List<MessageAttachmentRow> list = new Gee.ArrayList<MessageAttachmentRow>();
if (results.finished)
return list;
do {
list.add(new MessageAttachmentRow(this, results.fetch_int64(0), message_id,
results.fetch_string(1), results.fetch_string(2), results.fetch_int64(3)));
yield results.next_async();
check_cancel(cancellable, "list_async");
} while (!results.finished);
return list;
}
public async void remove_async(Transaction? transaction, int64 attachment_id,
Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction,
"MessageAttachmentTable.remove_async", cancellable);
SQLHeavy.Query query = locked.prepare(
"DELETE FROM MessageAttachmentTable WHERE attachment_id = ?");
query.bind_int64(0, attachment_id);
yield query.execute_async();
locked.set_commit_required();
yield release_lock_async(transaction, locked, cancellable);
check_cancel(cancellable, "remove_async");
}
}

View file

@ -132,8 +132,6 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
if ((email.fields & field) != 0)
set_from_email(field, email);
else
unset_fields(field);
}
}
@ -224,62 +222,5 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
this.fields = this.fields.set(Geary.Email.Field.PREVIEW);
}
}
private void unset_fields(Geary.Email.Field fields) {
if ((fields & Geary.Email.Field.DATE) != 0) {
date = null;
date_time_t = -1;
this.fields = this.fields.clear(Geary.Email.Field.DATE);
}
if ((fields & Geary.Email.Field.ORIGINATORS) != 0) {
from = null;
sender = null;
reply_to = null;
this.fields = this.fields.clear(Geary.Email.Field.ORIGINATORS);
}
if ((fields & Geary.Email.Field.RECEIVERS) != 0) {
to = null;
cc = null;
bcc = null;
this.fields = this.fields.clear(Geary.Email.Field.RECEIVERS);
}
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
message_id = null;
in_reply_to = null;
references = null;
this.fields = this.fields.clear(Geary.Email.Field.REFERENCES);
}
if ((fields & Geary.Email.Field.SUBJECT) != 0) {
subject = null;
this.fields = this.fields.clear(Geary.Email.Field.SUBJECT);
}
if ((fields & Geary.Email.Field.HEADER) != 0) {
header = null;
this.fields = this.fields.clear(Geary.Email.Field.HEADER);
}
if ((fields & Geary.Email.Field.BODY) != 0) {
body = null;
this.fields = this.fields.clear(Geary.Email.Field.BODY);
}
if ((fields & Geary.Email.Field.PREVIEW) != 0) {
preview = null;
this.fields = this.fields.clear(Geary.Email.Field.PREVIEW);
}
}
}