1/* 2 * Copyright 2016 Software Freedom Conservancy Inc. 3 * Copyright 2018 Michael Gratton <mike@vee.net> 4 * 5 * This software is licensed under the GNU Lesser General Public License 6 * (version 2.1 or later). See the COPYING file in this distribution. 7 */ 8 9/** 10 * An Email represents a single RFC 822 style email message. 11 * 12 * This class provides a common abstraction over different 13 * representations of email messages, allowing messages from different 14 * mail systems, from both local and remote sources, and locally 15 * composed email messages to all be represented by a single 16 * object. While this object represents a RFC 822 message, it also 17 * holds additional metadata about an email not specified by that 18 * format, such as its unique {@link id}, and unread state and other 19 * {@link email_flags}. 20 * 21 * Email objects may by constructed in many ways, but are usually 22 * obtained via a {@link Folder}. Email objects may be partial 23 * representations of messages, in cases where a remote message has 24 * not been fully downloaded, or a local message not fully loaded from 25 * a database. This can be checked via an email's {@link fields} 26 * property, and if the currently loaded fields are not sufficient, 27 * then additional fields can be loaded via a folder. 28 */ 29public class Geary.Email : BaseObject, EmailHeaderSet { 30 31 /** 32 * The maximum expected length of message body preview text. 33 */ 34 public const int MAX_PREVIEW_BYTES = 256; 35 36 /** 37 * Indicates email fields that may change over time. 38 * 39 * The mutable fields are: FLAGS -- since these change as for 40 * example messages are marked as read, and PREVIEW -- since the 41 * preview is updated when the full message body is 42 * available. All others never change once stored in the 43 * database. 44 */ 45 public const Field MUTABLE_FIELDS = ( 46 Geary.Email.Field.FLAGS | Geary.Email.Field.PREVIEW 47 ); 48 49 /** 50 * Indicates the email fields required to build an RFC822.Message. 51 * 52 * @see get_message 53 */ 54 public const Field REQUIRED_FOR_MESSAGE = ( 55 Geary.Email.Field.HEADER | Geary.Email.Field.BODY 56 ); 57 58 /** 59 * Specifies specific parts of an email message. 60 * 61 * See the {@link Email.fields} property to determine which parts 62 * an email object currently contains. 63 */ 64 public enum Field { 65 // THESE VALUES ARE PERSISTED. Change them only if you know what you're doing. 66 67 /** Denotes no fields. */ 68 NONE = 0, 69 70 /** The RFC 822 Date header. */ 71 DATE = 1 << 0, 72 73 /** The RFC 822 From, Sender, and Reply-To headers. */ 74 ORIGINATORS = 1 << 1, 75 76 /** The RFC 822 To, Cc, and Bcc headers. */ 77 RECEIVERS = 1 << 2, 78 79 /** The RFC 822 Message-Id, In-Reply-To, and References headers. */ 80 REFERENCES = 1 << 3, 81 82 /** The RFC 822 Subject header. */ 83 SUBJECT = 1 << 4, 84 85 /** The list of all RFC 822 headers. */ 86 HEADER = 1 << 5, 87 88 /** The RFC 822 message body and attachments. */ 89 BODY = 1 << 6, 90 91 /** The {@link Email.properties} object. */ 92 PROPERTIES = 1 << 7, 93 94 /** The plain text preview. */ 95 PREVIEW = 1 << 8, 96 97 /** The {@link Email.email_flags} object. */ 98 FLAGS = 1 << 9, 99 100 /** 101 * The union of the primary headers of a message. 102 * 103 * The envelope includes the {@link DATE}, {@link 104 * ORIGINATORS}, {@link RECEIVERS}, {@link REFERENCES}, and 105 * {@link SUBJECT} fields. 106 */ 107 ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT, 108 109 /** The union of all email fields. */ 110 ALL = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT | 111 HEADER | BODY | PROPERTIES | PREVIEW | FLAGS; 112 113 public static Field[] all() { 114 return { 115 DATE, 116 ORIGINATORS, 117 RECEIVERS, 118 REFERENCES, 119 SUBJECT, 120 HEADER, 121 BODY, 122 PROPERTIES, 123 PREVIEW, 124 FLAGS 125 }; 126 } 127 128 public inline bool is_all_set(Field required_fields) { 129 return (this & required_fields) == required_fields; 130 } 131 132 public inline bool is_any_set(Field required_fields) { 133 return (this & required_fields) != 0; 134 } 135 136 public inline Field set(Field field) { 137 return (this | field); 138 } 139 140 public inline Field clear(Field field) { 141 return (this & ~(field)); 142 } 143 144 public inline bool fulfills(Field required_fields) { 145 return is_all_set(required_fields); 146 } 147 148 public inline bool fulfills_any(Field required_fields) { 149 return is_any_set(required_fields); 150 } 151 152 public inline bool require(Field required_fields) { 153 return is_all_set(required_fields); 154 } 155 156 public inline bool requires_any(Field required_fields) { 157 return is_any_set(required_fields); 158 } 159 160 public string to_string() { 161 string value = "NONE"; 162 if (this == ALL) { 163 value = "ALL"; 164 } else if (this > 0) { 165 StringBuilder builder = new StringBuilder(); 166 foreach (Field f in all()) { 167 if (is_all_set(f)) { 168 if (!String.is_empty(builder.str)) { 169 builder.append(","); 170 } 171 builder.append( 172 ObjectUtils.to_enum_nick(typeof(Field), f).up() 173 ); 174 } 175 } 176 value = builder.str; 177 } 178 return value; 179 } 180 } 181 182 /** 183 * A unique identifier for the Email in the Folder. 184 * 185 * This is is guaranteed to be unique for as long as the Folder is 186 * open. Once closed, guarantees are no longer made. 187 * 188 * This field is always returned, no matter what Fields are used 189 * to retrieve the Email. 190 */ 191 public Geary.EmailIdentifier id { get; private set; } 192 193 /** 194 * {@inheritDoc} 195 * 196 * Value will be valid if {@link Field.ORIGINATORS} is set. 197 */ 198 public RFC822.MailboxAddresses? from { get { return this._from; } } 199 private RFC822.MailboxAddresses? _from = null; 200 201 /** 202 * {@inheritDoc} 203 * 204 * Value will be valid if {@link Field.ORIGINATORS} is set. 205 */ 206 public RFC822.MailboxAddress? sender { get { return this._sender; } } 207 private RFC822.MailboxAddress? _sender = null; 208 209 /** 210 * {@inheritDoc} 211 * 212 * Value will be valid if {@link Field.ORIGINATORS} is set. 213 */ 214 public RFC822.MailboxAddresses? reply_to { get { return this._reply_to; } } 215 private RFC822.MailboxAddresses? _reply_to = null; 216 217 /** 218 * {@inheritDoc} 219 * 220 * Value will be valid if {@link Field.RECEIVERS} is set. 221 */ 222 public RFC822.MailboxAddresses? to { get { return this._to; } } 223 private RFC822.MailboxAddresses? _to = null; 224 225 /** 226 * {@inheritDoc} 227 * 228 * Value will be valid if {@link Field.RECEIVERS} is set. 229 */ 230 public RFC822.MailboxAddresses? cc { get { return this._cc; } } 231 private RFC822.MailboxAddresses? _cc = null; 232 233 /** 234 * {@inheritDoc} 235 * 236 * Value will be valid if {@link Field.RECEIVERS} is set. 237 */ 238 public RFC822.MailboxAddresses? bcc { get { return this._bcc; } } 239 private RFC822.MailboxAddresses? _bcc = null; 240 241 /** 242 * {@inheritDoc} 243 * 244 * Value will be valid if {@link Field.REFERENCES} is set. 245 */ 246 public RFC822.MessageID? message_id { get { return this._message_id; } } 247 private RFC822.MessageID? _message_id = null; 248 249 /** 250 * {@inheritDoc} 251 * 252 * Value will be valid if {@link Field.REFERENCES} is set. 253 */ 254 public RFC822.MessageIDList? in_reply_to { get { return this._in_reply_to; } } 255 private RFC822.MessageIDList? _in_reply_to = null; 256 257 /** 258 * {@inheritDoc} 259 * 260 * Value will be valid if {@link Field.REFERENCES} is set. 261 */ 262 public RFC822.MessageIDList? references { get { return this._references; } } 263 private RFC822.MessageIDList? _references = null; 264 265 /** 266 * {@inheritDoc} 267 * 268 * Value will be valid if {@link Field.SUBJECT} is set. 269 */ 270 public RFC822.Subject? subject { get { return this._subject; } } 271 private RFC822.Subject? _subject = null; 272 273 /** 274 * {@inheritDoc} 275 * 276 * Value will be valid if {@link Field.DATE} is set. 277 */ 278 public RFC822.Date? date { get { return this._date; } } 279 private RFC822.Date? _date = null; 280 281 /** 282 * {@inheritDoc} 283 * 284 * Value will be valid if {@link Field.HEADER} is set. 285 */ 286 public RFC822.Header? header { get; protected set; default = null; } 287 288 /** 289 * The complete RFC 822 message body. 290 * 291 * Value will be valid if {@link Field.BODY} is set. 292 */ 293 public RFC822.Text? body { get; private set; default = null; } 294 295 /** 296 * MIME multipart body parts. 297 * 298 * Value will be valid if {@link Field.BODY} is set. 299 */ 300 public Gee.List<Geary.Attachment> attachments { get; private set; 301 default = new Gee.ArrayList<Geary.Attachment>(); } 302 303 /** 304 * A plain text prefix of the email's message body. 305 * 306 * Value will be valid if {@link Field.PREVIEW} is set. 307 */ 308 public RFC822.PreviewText? preview { get; private set; default = null; } 309 310 /** 311 * Set of immutable properties for the email. 312 * 313 * Value will be valid if {@link Field.PROPERTIES} is set. 314 */ 315 public Geary.EmailProperties? properties { get; private set; default = null; } 316 317 /** 318 * Set of mutable flags for the email. 319 * 320 * Value will be valid if {@link Field.FLAGS} is set. 321 */ 322 public Geary.EmailFlags? email_flags { get; private set; default = null; } 323 324 /** 325 * Specifies the properties that have been populated for this email. 326 * 327 * Since this email object may be a partial representation of a 328 * complete email message, this property lists all parts of the 329 * object that have actually been loaded, as opposed to parts that 330 * are simply missing from the email it represents. 331 * 332 * For example, if this property includes the {@link 333 * Field.SUBJECT} flag, then the {@link subject} property has been 334 * set to reflect the Subject header of the message. Of course, 335 * the subject may then still may be null or empty, if the email 336 * did not specify a subject header. 337 */ 338 public Geary.Email.Field fields { get; private set; default = Field.NONE; } 339 340 341 private Geary.RFC822.Message? message = null; 342 343 344 /** Constructs a new, empty email with the given id. */ 345 public Email(Geary.EmailIdentifier id) { 346 this.id = id; 347 } 348 349 /** 350 * Construct a Geary.Email from a complete RFC822 message. 351 */ 352 public Email.from_message(EmailIdentifier id, 353 RFC822.Message message) throws GLib.Error { 354 this(id); 355 set_send_date(message.date); 356 set_originators(message.from, message.sender, message.reply_to); 357 set_receivers(message.to, message.cc, message.bcc); 358 set_full_references( 359 message.message_id, message.in_reply_to, message.references 360 ); 361 set_message_subject(message.subject); 362 set_message_header(message.get_header()); 363 set_message_body(message.get_body()); 364 string preview = message.get_preview(); 365 if (!String.is_empty_or_whitespace(preview)) { 366 set_message_preview(new RFC822.PreviewText.from_string(preview)); 367 } 368 369 // Set this last as the methods above would reset it otherwise 370 this.message = message; 371 } 372 373 /** 374 * Determines if this message is unread from its flags. 375 * 376 * If {@link email_flags} is not null, returns the value of {@link 377 * EmailFlags.is_unread}, otherwise returns {@link 378 * Trillian.UNKNOWN}. 379 */ 380 public inline Trillian is_unread() { 381 return email_flags != null ? Trillian.from_boolean(email_flags.is_unread()) : Trillian.UNKNOWN; 382 } 383 384 /** 385 * Determines if this message is flagged from its flags. 386 * 387 * If {@link email_flags} is not null, returns the value of {@link 388 * EmailFlags.is_flagged}, otherwise returns {@link 389 * Trillian.UNKNOWN}. 390 */ 391 public inline Trillian is_flagged() { 392 return email_flags != null ? Trillian.from_boolean(email_flags.is_flagged()) : Trillian.UNKNOWN; 393 } 394 395 /** 396 * Determines if this message is flagged from its flags. 397 * 398 * If {@link email_flags} is not null, returns the value of {@link 399 * EmailFlags.load_remote_images}, otherwise returns {@link 400 * Trillian.UNKNOWN}. 401 */ 402 public inline Trillian load_remote_images() { 403 return email_flags != null ? Trillian.from_boolean(email_flags.load_remote_images()) : Trillian.UNKNOWN; 404 } 405 406 public void set_send_date(RFC822.Date? date) { 407 this._date = date; 408 409 this.message = null; 410 this.fields |= Field.DATE; 411 } 412 413 /** 414 * Sets the RFC822 originators for the message. 415 * 416 * RFC 2822 requires at least one From address, that the Sender 417 * and From not be identical, and that both From and ReplyTo are 418 * optional. 419 */ 420 public void set_originators(RFC822.MailboxAddresses? from, 421 RFC822.MailboxAddress? sender, 422 RFC822.MailboxAddresses? reply_to) 423 throws Error { 424 // XXX Should be throwing an error here if from is empty or 425 // sender is same as from 426 this._from = from; 427 this._sender = sender; 428 this._reply_to = reply_to; 429 430 this.message = null; 431 this.fields |= Field.ORIGINATORS; 432 } 433 434 public void set_receivers(RFC822.MailboxAddresses? to, 435 RFC822.MailboxAddresses? cc, 436 RFC822.MailboxAddresses? bcc) { 437 this._to = to; 438 this._cc = cc; 439 this._bcc = bcc; 440 441 this.message = null; 442 this.fields |= Field.RECEIVERS; 443 } 444 445 public void set_full_references(RFC822.MessageID? message_id, 446 RFC822.MessageIDList? in_reply_to, 447 Geary.RFC822.MessageIDList? references) { 448 this._message_id = message_id; 449 this._in_reply_to = in_reply_to; 450 this._references = references; 451 452 this.message = null; 453 this.fields |= Field.REFERENCES; 454 } 455 456 public void set_message_subject(Geary.RFC822.Subject? subject) { 457 this._subject = subject; 458 459 this.message = null; 460 this.fields |= Field.SUBJECT; 461 } 462 463 public void set_message_header(Geary.RFC822.Header header) { 464 this.header = header; 465 466 this.message = null; 467 fields |= Field.HEADER; 468 } 469 470 public void set_message_body(Geary.RFC822.Text body) { 471 this.body = body; 472 473 this.message = null; 474 fields |= Field.BODY; 475 } 476 477 public void set_email_properties(Geary.EmailProperties properties) { 478 this.properties = properties; 479 480 fields |= Field.PROPERTIES; 481 } 482 483 public void set_message_preview(Geary.RFC822.PreviewText preview) { 484 this.preview = preview; 485 486 fields |= Field.PREVIEW; 487 } 488 489 public void set_flags(Geary.EmailFlags email_flags) { 490 this.email_flags = email_flags; 491 492 fields |= Field.FLAGS; 493 } 494 495 public void add_attachment(Geary.Attachment attachment) { 496 attachments.add(attachment); 497 } 498 499 public void add_attachments(Gee.Collection<Geary.Attachment> attachments) { 500 this.attachments.add_all(attachments); 501 } 502 503 public string get_searchable_attachment_list() { 504 StringBuilder search = new StringBuilder(); 505 foreach (Geary.Attachment attachment in attachments) { 506 if (attachment.has_content_filename) { 507 search.append(attachment.content_filename); 508 search.append("\n"); 509 } 510 } 511 return search.str; 512 } 513 514 /** 515 * Constructs a new RFC 822 message from this email. 516 * 517 * This method requires the {@link REQUIRED_FOR_MESSAGE} fields be 518 * present. If not, {@link EngineError.INCOMPLETE_MESSAGE} is 519 * thrown. 520 */ 521 public Geary.RFC822.Message get_message() throws EngineError, Error { 522 if (this.message == null) { 523 if (!fields.fulfills(REQUIRED_FOR_MESSAGE)) { 524 throw new EngineError.INCOMPLETE_MESSAGE( 525 "Parsed email requires HEADER and BODY" 526 ); 527 } 528 this.message = new Geary.RFC822.Message.from_parts(header, body); 529 } 530 return this.message; 531 } 532 533 /** 534 * Returns the attachment with the given MIME Content ID. 535 * 536 * Requires the REQUIRED_FOR_MESSAGE fields be present; else 537 * EngineError.INCOMPLETE_MESSAGE is thrown. 538 */ 539 public Geary.Attachment? get_attachment_by_content_id(string cid) 540 throws EngineError { 541 if (!fields.fulfills(REQUIRED_FOR_MESSAGE)) 542 throw new EngineError.INCOMPLETE_MESSAGE("Parsed email requires HEADER and BODY"); 543 544 foreach (Geary.Attachment attachment in attachments) { 545 if (attachment.content_id == cid) { 546 return attachment; 547 } 548 } 549 return null; 550 } 551 552 /** 553 * Returns a list of this email's ancestry by Message-ID. IDs are not returned in any 554 * particular order. The ancestry is made up from this email's Message-ID, its References, 555 * and its In-Reply-To. Thus, this email must have been fetched with Field.REFERENCES for 556 * this method to return a complete list. 557 */ 558 public Gee.Set<RFC822.MessageID>? get_ancestors() { 559 Gee.Set<RFC822.MessageID> ancestors = new Gee.HashSet<RFC822.MessageID>(); 560 561 // the email's Message-ID counts as its lineage 562 if (message_id != null) 563 ancestors.add(message_id); 564 565 // References list the email trail back to its source 566 if (references != null) 567 ancestors.add_all(references.get_all()); 568 569 // RFC822 requires the In-Reply-To Message-ID be prepended to the References list, but 570 // this ensures that's the case 571 if (in_reply_to != null) 572 ancestors.add_all(in_reply_to.get_all()); 573 574 return (ancestors.size > 0) ? ancestors : null; 575 } 576 577 public string get_preview_as_string() { 578 return (preview != null) ? preview.buffer.to_string() : ""; 579 } 580 581 public string to_string() { 582 return "[%s] ".printf(id.to_string()); 583 } 584 585 /** 586 * Converts a Collection of {@link Email}s to a Map of Emails keyed by {@link EmailIdentifier}s. 587 * 588 * @return null if emails is empty or null. 589 */ 590 public static Gee.Map<Geary.EmailIdentifier, Geary.Email>? emails_to_map(Gee.Collection<Geary.Email>? emails) { 591 if (emails == null || emails.size == 0) 592 return null; 593 594 Gee.Map<Geary.EmailIdentifier, Geary.Email> map = new Gee.HashMap<Geary.EmailIdentifier, 595 Geary.Email>(); 596 foreach (Email email in emails) 597 map.set(email.id, email); 598 599 return map; 600 } 601 602 /** 603 * CompareFunc to sort {@link Email} by {@link date} ascending. 604 * 605 * If the date field is unavailable on either Email, their identifiers are compared to 606 * stabilize the sort. 607 */ 608 public static int compare_sent_date_ascending(Geary.Email aemail, Geary.Email bemail) { 609 if (aemail.date == null || bemail.date == null) { 610 GLib.message("Warning: comparing email for sent date but no Date: field loaded"); 611 612 return compare_id_ascending(aemail, bemail); 613 } 614 615 int compare = aemail.date.value.compare(bemail.date.value); 616 617 // stabilize sort by using the mail identifier's stable sort ordering 618 return (compare != 0) ? compare : compare_id_ascending(aemail, bemail); 619 } 620 621 /** 622 * CompareFunc to sort {@link Email} by {@link date} descending. 623 * 624 * If the date field is unavailable on either Email, their identifiers are compared to 625 * stabilize the sort. 626 */ 627 public static int compare_sent_date_descending(Geary.Email aemail, Geary.Email bemail) { 628 return compare_sent_date_ascending(bemail, aemail); 629 } 630 631 /** 632 * CompareFunc to sort {@link Email} by {@link EmailProperties.date_received} ascending. 633 * 634 * If {@link properties} is unavailable on either Email, their identifiers are compared to 635 * stabilize the sort. 636 */ 637 public static int compare_recv_date_ascending(Geary.Email aemail, Geary.Email bemail) { 638 if (aemail.properties == null || bemail.properties == null) { 639 GLib.message("Warning: comparing email for received date but email properties not loaded"); 640 641 return compare_id_ascending(aemail, bemail); 642 } 643 644 int compare = aemail.properties.date_received.compare(bemail.properties.date_received); 645 646 // stabilize sort with identifiers 647 return (compare != 0) ? compare : compare_id_ascending(aemail, bemail); 648 } 649 650 /** 651 * CompareFunc to sort {@link Email} by {@link EmailProperties.date_received} descending. 652 * 653 * If {@link properties} is unavailable on either Email, their identifiers are compared to 654 * stabilize the sort. 655 */ 656 public static int compare_recv_date_descending(Geary.Email aemail, Geary.Email bemail) { 657 return compare_recv_date_ascending(bemail, aemail); 658 } 659 660 // only used to stabilize a sort 661 private static int compare_id_ascending(Geary.Email aemail, Geary.Email bemail) { 662 return aemail.id.stable_sort_comparator(bemail.id); 663 } 664 665 /** 666 * CompareFunc to sort Email by EmailProperties.total_bytes. If not available, emails are 667 * compared by EmailIdentifier. 668 */ 669 public static int compare_size_ascending(Geary.Email aemail, Geary.Email bemail) { 670 Geary.EmailProperties? aprop = (Geary.EmailProperties) aemail.properties; 671 Geary.EmailProperties? bprop = (Geary.EmailProperties) bemail.properties; 672 673 if (aprop == null || bprop == null) { 674 GLib.message("Warning: comparing email by size but email properties not loaded"); 675 676 return compare_id_ascending(aemail, bemail); 677 } 678 679 int cmp = (int) (aprop.total_bytes - bprop.total_bytes).clamp(-1, 1); 680 681 return (cmp != 0) ? cmp : compare_id_ascending(aemail, bemail); 682 } 683 684 /** 685 * CompareFunc to sort Email by EmailProperties.total_bytes. If not available, emails are 686 * compared by EmailIdentifier. 687 */ 688 public static int compare_size_descending(Geary.Email aemail, Geary.Email bemail) { 689 return compare_size_ascending(bemail, aemail); 690 } 691} 692 693