1/* 2 * Copyright © 2016 Software Freedom Conservancy Inc. 3 * Copyright © 2018, 2020 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/** 11 * An interface between the high-level engine API and an IMAP mailbox. 12 * 13 * Because of the complexities of the IMAP protocol, class takes 14 * common operations that a Geary.Folder implementation would need 15 * (in particular, {@link Geary.ImapEngine.MinimalFolder}) and makes 16 * them into simple async calls. 17 * 18 * When constructed, this class will issue an IMAP SELECT command for 19 * the mailbox represented by this folder, placing the session in the 20 * Selected state. 21 */ 22private class Geary.Imap.FolderSession : Geary.Imap.SessionObject { 23 24 private const Geary.Email.Field BASIC_FETCH_FIELDS = Email.Field.ENVELOPE | Email.Field.DATE 25 | Email.Field.ORIGINATORS | Email.Field.RECEIVERS | Email.Field.REFERENCES 26 | Email.Field.SUBJECT | Email.Field.HEADER; 27 28 29 /** The folder this session operates on. */ 30 public Imap.Folder folder { get; private set; } 31 32 /** Determines if this folder immutable. */ 33 public Trillian readonly { get; private set; default = Trillian.UNKNOWN; } 34 35 /** This folder's set of permanent IMAP flags. */ 36 public MessageFlags? permanent_flags { get; private set; default = null; } 37 38 /** Determines if this folder accepts custom IMAP flags. */ 39 public Trillian accepts_user_flags { get; private set; default = Trillian.UNKNOWN; } 40 41 private MailboxSpecifier mailbox; 42 43 private Quirks quirks; 44 45 private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex(); 46 private Gee.HashMap<SequenceNumber, FetchedData>? fetch_accumulator = null; 47 private Gee.Set<Imap.UID>? search_accumulator = null; 48 49 /** 50 * A (potentially unsolicited) response from the server. 51 * 52 * See [[http://tools.ietf.org/html/rfc3501#section-7.3.1]] 53 */ 54 public signal void exists(int total); 55 56 /** 57 * A (potentially unsolicited) response from the server. 58 * 59 * See [[http://tools.ietf.org/html/rfc3501#section-7.3.2]] 60 */ 61 public signal void recent(int total); 62 63 /** 64 * A (potentially unsolicited) response from the server. 65 * 66 * See [[http://tools.ietf.org/html/rfc3501#section-7.4.1]] 67 */ 68 public signal void expunge(SequenceNumber position); 69 70 /** 71 * Fabricated from the IMAP signals and state obtained at open_async(). 72 */ 73 public signal void appended(int count); 74 75 /** 76 * Fabricated from the IMAP signals and state obtained at open_async(). 77 */ 78 public signal void updated(SequenceNumber pos, FetchedData data); 79 80 /** 81 * Fabricated from the IMAP signals and state obtained at open_async(). 82 */ 83 public signal void removed(SequenceNumber pos); 84 85 86 public async FolderSession(ClientSession session, 87 Imap.Folder folder, 88 GLib.Cancellable? cancellable) 89 throws GLib.Error { 90 base(session); 91 this.folder = folder; 92 this.quirks = session.quirks; 93 94 if (folder.properties.attrs.is_no_select) { 95 throw new ImapError.NOT_SUPPORTED( 96 "Folder cannot be selected: %s", 97 folder.path.to_string() 98 ); 99 } 100 101 // Update based on our current session 102 folder.properties.set_from_session_capabilities(session.capabilities); 103 104 // connect to interesting signals *before* selecting 105 session.exists.connect(on_exists); 106 session.expunge.connect(on_expunge); 107 session.fetch.connect(on_fetch); 108 session.recent.connect(on_recent); 109 session.search.connect(on_search); 110 session.status_response_received.connect(on_status_response); 111 112 this.mailbox = session.get_mailbox_for_path(folder.path); 113 StatusResponse? response = yield session.select_async( 114 this.mailbox, cancellable 115 ); 116 throw_on_not_ok(response, "SELECT " + this.folder.path.to_string()); 117 118 // if at end of SELECT command accepts_user_flags is still 119 // UNKKNOWN, treat as TRUE because, according to IMAP spec, if 120 // PERMANENTFLAGS are not returned, then assume OK 121 if (this.accepts_user_flags == Trillian.UNKNOWN) 122 this.accepts_user_flags = Trillian.TRUE; 123 } 124 125 /** 126 * Enables IMAP IDLE for the session, if supported. 127 */ 128 public async void enable_idle(Cancellable? cancellable) 129 throws Error { 130 ClientSession session = get_session(); 131 int token = yield this.cmd_mutex.claim_async(cancellable); 132 Error? cmd_err = null; 133 try { 134 session.enable_idle(); 135 } catch (Error err) { 136 cmd_err = err; 137 } 138 139 this.cmd_mutex.release(ref token); 140 141 if (cmd_err != null) { 142 throw cmd_err; 143 } 144 } 145 146 /** {@inheritDoc} */ 147 public override ClientSession? close() { 148 ClientSession? old_session = base.close(); 149 if (old_session != null) { 150 old_session.exists.disconnect(on_exists); 151 old_session.expunge.disconnect(on_expunge); 152 old_session.fetch.disconnect(on_fetch); 153 old_session.recent.disconnect(on_recent); 154 old_session.search.disconnect(on_search); 155 old_session.status_response_received.disconnect(on_status_response); 156 } 157 return old_session; 158 } 159 160 /** Sends a NOOP command. */ 161 public async void send_noop(GLib.Cancellable? cancellable) 162 throws GLib.Error { 163 yield exec_commands_async( 164 Collection.single(new NoopCommand(cancellable)), 165 null, 166 null, 167 cancellable 168 ); 169 } 170 171 private void on_exists(int total) { 172 debug("EXISTS %d", total); 173 174 int old_total = this.folder.properties.select_examine_messages; 175 this.folder.properties.set_select_examine_message_count(total); 176 177 exists(total); 178 if (old_total >= 0 && old_total < total) { 179 appended(total - old_total); 180 } 181 } 182 183 private void on_expunge(SequenceNumber pos) { 184 debug("EXPUNGE %s", pos.to_string()); 185 186 int old_total = this.folder.properties.select_examine_messages; 187 if (old_total > 0) { 188 this.folder.properties.set_select_examine_message_count( 189 old_total - 1 190 ); 191 } 192 193 expunge(pos); 194 removed(pos); 195 } 196 197 private void on_fetch(FetchedData data) { 198 // add if not found, merge if already received data for this email 199 if (this.fetch_accumulator != null) { 200 FetchedData? existing = this.fetch_accumulator.get(data.seq_num); 201 this.fetch_accumulator.set( 202 data.seq_num, (existing != null) ? data.combine(existing) : data 203 ); 204 } else { 205 debug("FETCH (unsolicited): %s:", data.to_string()); 206 updated(data.seq_num, data); 207 } 208 } 209 210 private void on_recent(int total) { 211 debug("RECENT %d", total); 212 this.folder.properties.recent = total; 213 recent(total); 214 } 215 216 private void on_search(int64[] seq_or_uid) { 217 // All SEARCH from this class are UID SEARCH, so can reliably convert and add to 218 // accumulator 219 if (this.search_accumulator != null) { 220 foreach (int64 uid in seq_or_uid) { 221 try { 222 this.search_accumulator.add(new UID.checked(uid)); 223 } catch (ImapError imaperr) { 224 warning("Unable to process SEARCH UID result: %s", 225 imaperr.message); 226 } 227 } 228 } else { 229 debug("Not handling unsolicited SEARCH response"); 230 } 231 } 232 233 private void on_status_response(StatusResponse status_response) { 234 // only interested in ResponseCodes here 235 ResponseCode? response_code = status_response.response_code; 236 if (response_code == null) 237 return; 238 239 try { 240 // Have to take a copy of the string property before evaluation due to this bug: 241 // https://bugzilla.gnome.org/show_bug.cgi?id=703818 242 string value = response_code.get_response_code_type().value; 243 switch (value) { 244 case ResponseCodeType.READONLY: 245 this.readonly = Trillian.TRUE; 246 break; 247 248 case ResponseCodeType.READWRITE: 249 this.readonly = Trillian.FALSE; 250 break; 251 252 case ResponseCodeType.UIDNEXT: 253 try { 254 this.folder.properties.uid_next = response_code.get_uid_next(); 255 } catch (ImapError.INVALID err) { 256 // Some mail servers e.g hMailServer and 257 // whatever is used by home.pl (dovecot?) 258 // sends UIDNEXT 0. Just ignore these since 259 // there nothing else that can be done. See 260 // GNOME/geary#711 261 if (response_code.get_as_string(1).as_int64() == 0) { 262 warning("Ignoring bad UIDNEXT 0 from server"); 263 } else { 264 throw err; 265 } 266 } 267 break; 268 269 case ResponseCodeType.UIDVALIDITY: 270 this.folder.properties.uid_validity = response_code.get_uid_validity(); 271 break; 272 273 case ResponseCodeType.UNSEEN: 274 // do NOT update properties.unseen, as the UNSEEN response code (here) means 275 // the sequence number of the first unseen message, not the total count of 276 // unseen messages 277 break; 278 279 case ResponseCodeType.PERMANENT_FLAGS: 280 this.permanent_flags = response_code.get_permanent_flags(); 281 this.accepts_user_flags = Trillian.from_boolean( 282 this.permanent_flags.contains(MessageFlag.ALLOWS_NEW) 283 ); 284 break; 285 286 default: 287 // ignored 288 break; 289 } 290 } catch (ImapError ierr) { 291 warning("Unable to parse ResponseCode %s: %s", response_code.to_string(), 292 ierr.message); 293 } 294 } 295 296 // Executes a set of commands. 297 // 298 // All commands must executed inside the cmd_mutex. Collects 299 // results in fetch_results or store results. 300 private async Gee.Map<Command,StatusResponse>? 301 exec_commands_async(Gee.Collection<Command> cmds, 302 Gee.HashMap<SequenceNumber, FetchedData>? fetch_results, 303 Gee.Set<Imap.UID>? search_results, 304 GLib.Cancellable? cancellable) 305 throws GLib.Error { 306 ClientSession session = get_session(); 307 Gee.Map<Command, StatusResponse>? responses = null; 308 int token = yield this.cmd_mutex.claim_async(cancellable); 309 310 this.fetch_accumulator = fetch_results; 311 this.search_accumulator = search_results; 312 313 Error? cmd_err = null; 314 try { 315 responses = yield session.send_multiple_commands_async( 316 cmds, cancellable 317 ); 318 } catch (Error err) { 319 cmd_err = err; 320 } 321 322 this.fetch_accumulator = null; 323 this.search_accumulator = null; 324 325 this.cmd_mutex.release(ref token); 326 327 if (cmd_err != null) { 328 throw cmd_err; 329 } 330 331 foreach (Command cmd in responses.keys) { 332 throw_on_not_ok(responses.get(cmd), cmd.to_string()); 333 } 334 335 return responses; 336 } 337 338 // Utility method for listing UIDs on the remote within the supplied range 339 public async Gee.Set<Imap.UID>? list_uids_async(MessageSet msg_set, 340 GLib.Cancellable? cancellable) 341 throws GLib.Error { 342 // Although FETCH could be used, SEARCH is more efficient in returning pure UID results, 343 // which is all we're interested in here 344 SearchCriteria criteria = new SearchCriteria(SearchCriterion.message_set(msg_set)); 345 SearchCommand cmd = new SearchCommand.uid(criteria, cancellable); 346 347 Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>(); 348 yield exec_commands_async( 349 Geary.iterate<Command>(cmd).to_array_list(), 350 null, 351 search_results, 352 cancellable 353 ); 354 355 return (search_results.size > 0) ? search_results : null; 356 } 357 358 private Gee.Collection<FetchCommand> assemble_list_commands( 359 Imap.MessageSet msg_set, 360 Geary.Email.Field fields, 361 GLib.Cancellable? cancellable, 362 out FetchBodyDataSpecifier[]? header_specifiers, 363 out FetchBodyDataSpecifier? body_specifier, 364 out FetchBodyDataSpecifier? preview_specifier, 365 out FetchBodyDataSpecifier? preview_charset_specifier 366 ) { 367 // getting all the fields can require multiple FETCH commands (some servers don't handle 368 // well putting every required data item into single command), so aggregate FetchCommands 369 Gee.Collection<FetchCommand> cmds = new Gee.ArrayList<FetchCommand>(); 370 371 // if not a UID FETCH, request UIDs for all messages so their EmailIdentifier can be 372 // created without going back to the database (assuming the messages have already been 373 // pulled down, not a guarantee); if request is for NONE, that guarantees that the 374 // EmailIdentifier will be set, and so fetch UIDs (which looks funny but works when 375 // listing a range for contents: UID FETCH x:y UID) 376 if (!msg_set.is_uid || fields == Geary.Email.Field.NONE) { 377 cmds.add( 378 new FetchCommand.data_type( 379 msg_set, FetchDataSpecifier.UID, cancellable 380 ) 381 ); 382 } 383 384 // convert bulk of the "basic" fields into a one or two FETCH commands (some servers have 385 // exhibited bugs or return NO when too many FETCH data types are combined on a single 386 // command) 387 header_specifiers = null; 388 if (fields.requires_any(BASIC_FETCH_FIELDS)) { 389 Gee.List<FetchDataSpecifier> basic_types = 390 new Gee.LinkedList<FetchDataSpecifier>(); 391 Gee.List<string> header_fields = new Gee.LinkedList<string>(); 392 393 fields_to_fetch_data_types(fields, basic_types, header_fields); 394 395 // Add all simple data types as one FETCH command 396 if (!basic_types.is_empty) { 397 cmds.add( 398 new FetchCommand(msg_set, basic_types, null, cancellable) 399 ); 400 } 401 402 // Add all header field requests as separate FETCH 403 // command(s). If the HEADER.FIELDS hack is enabled and 404 // there is more than one header that needs fetching, we 405 // need to send multiple commands since we can't separate 406 // them by spaces in the same command. 407 // 408 // See <https://gitlab.gnome.org/GNOME/geary/issues/571> 409 if (!header_fields.is_empty) { 410 if (!this.quirks.fetch_header_part_no_space || 411 header_fields.size == 1) { 412 header_specifiers = new FetchBodyDataSpecifier[1]; 413 header_specifiers[0] = new FetchBodyDataSpecifier.peek( 414 FetchBodyDataSpecifier.SectionPart.HEADER_FIELDS, 415 null, 416 -1, 417 -1, 418 header_fields.to_array() 419 ); 420 } else { 421 header_specifiers = new FetchBodyDataSpecifier[header_fields.size]; 422 int i = 0; 423 foreach (string name in header_fields) { 424 header_specifiers[i++] = new FetchBodyDataSpecifier.peek( 425 FetchBodyDataSpecifier.SectionPart.HEADER_FIELDS, 426 null, 427 -1, 428 -1, 429 new string[] { name } 430 ); 431 } 432 } 433 434 foreach (FetchBodyDataSpecifier header in header_specifiers) { 435 if (this.quirks.fetch_header_part_no_space) { 436 header.omit_request_header_fields_space(); 437 } 438 cmds.add( 439 new FetchCommand.body_data_type( 440 msg_set, header, cancellable 441 ) 442 ); 443 } 444 } 445 } 446 447 // RFC822 BODY is a separate command 448 if (fields.require(Email.Field.BODY)) { 449 body_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.TEXT, 450 null, -1, -1, null); 451 452 cmds.add( 453 new FetchCommand.body_data_type( 454 msg_set, body_specifier, cancellable 455 ) 456 ); 457 } else { 458 body_specifier = null; 459 } 460 461 // PREVIEW obtains the content type and a truncated version of 462 // the first part of the message, which often leads to poor 463 // results. It can also be also be synthesised from the 464 // email's RFC822 message in fetched_data_to_email, if the 465 // fields needed for reconstructing the RFC822 message are 466 // present. If so, rely on that and don't also request any 467 // additional data for the preview here. 468 if (fields.require(Email.Field.PREVIEW) && 469 !fields.require(Email.REQUIRED_FOR_MESSAGE)) { 470 // Get the preview text (the initial MAX_PREVIEW_BYTES of 471 // the first MIME section 472 473 preview_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.NONE, 474 { 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null); 475 cmds.add( 476 new FetchCommand.body_data_type( 477 msg_set, preview_specifier, cancellable 478 ) 479 ); 480 481 // Also get the character set to properly decode it 482 preview_charset_specifier = new FetchBodyDataSpecifier.peek( 483 FetchBodyDataSpecifier.SectionPart.MIME, { 1 }, -1, -1, null); 484 cmds.add( 485 new FetchCommand.body_data_type( 486 msg_set, preview_charset_specifier, cancellable 487 ) 488 ); 489 } else { 490 preview_specifier = null; 491 preview_charset_specifier = null; 492 } 493 494 // PROPERTIES and FLAGS are a separate command 495 if (fields.requires_any(Email.Field.PROPERTIES | Email.Field.FLAGS)) { 496 Gee.List<FetchDataSpecifier> data_types = new Gee.ArrayList<FetchDataSpecifier>(); 497 498 if (fields.require(Geary.Email.Field.PROPERTIES)) { 499 data_types.add(FetchDataSpecifier.INTERNALDATE); 500 data_types.add(FetchDataSpecifier.RFC822_SIZE); 501 } 502 503 if (fields.require(Geary.Email.Field.FLAGS)) 504 data_types.add(FetchDataSpecifier.FLAGS); 505 506 cmds.add(new FetchCommand(msg_set, data_types, null, cancellable)); 507 } 508 509 return cmds; 510 } 511 512 // Returns a no-message-id ImapDB.EmailIdentifier with the UID stored in it. 513 public async Gee.List<Geary.Email>? list_email_async(MessageSet msg_set, 514 Geary.Email.Field fields, 515 Cancellable? cancellable) 516 throws Error { 517 Gee.HashMap<SequenceNumber, FetchedData> fetched = 518 new Gee.HashMap<SequenceNumber, FetchedData>(); 519 FetchBodyDataSpecifier[]? header_specifiers = null; 520 FetchBodyDataSpecifier? body_specifier = null; 521 FetchBodyDataSpecifier? preview_specifier = null; 522 FetchBodyDataSpecifier? preview_charset_specifier = null; 523 bool success = false; 524 while (!success) { 525 Gee.Collection<FetchCommand> cmds = assemble_list_commands( 526 msg_set, 527 fields, 528 cancellable, 529 out header_specifiers, 530 out body_specifier, 531 out preview_specifier, 532 out preview_charset_specifier 533 ); 534 if (cmds.size == 0) { 535 throw new ImapError.INVALID( 536 "No FETCH commands generate for list request %s %s", 537 msg_set.to_string(), 538 fields.to_string() 539 ); 540 } 541 542 // Commands prepped, do the fetch and accumulate all the responses 543 try { 544 yield exec_commands_async(cmds, fetched, null, cancellable); 545 success = true; 546 } catch (ImapError.SERVER_ERROR err) { 547 if (retry_bad_header_fields_response(cmds)) { 548 // The command failed, but it wasn't using the 549 // header field hack, so retry it. 550 debug("Retryable server failure detected: %s", err.message); 551 } else { 552 throw err; 553 } 554 } 555 } 556 557 if (fetched.size == 0) 558 return null; 559 560 // Convert fetched data into Geary.Email objects 561 // because this could be for a lot of email, do in a background thread 562 Gee.List<Geary.Email> email_list = new Gee.ArrayList<Geary.Email>(); 563 yield Nonblocking.Concurrent.global.schedule_async(() => { 564 foreach (SequenceNumber seq_num in fetched.keys) { 565 FetchedData fetched_data = fetched.get(seq_num); 566 567 // the UID should either have been fetched (if using positional addressing) or should 568 // have come back with the response (if using UID addressing) 569 UID? uid = fetched_data.data_map.get(FetchDataSpecifier.UID) as UID; 570 if (uid == null) { 571 message("Unable to list message #%s: No UID returned from server", 572 seq_num.to_string()); 573 574 continue; 575 } 576 577 try { 578 Geary.Email email = fetched_data_to_email( 579 uid, 580 fetched_data, 581 fields, 582 header_specifiers, 583 body_specifier, 584 preview_specifier, 585 preview_charset_specifier 586 ); 587 if (!email.fields.fulfills(fields)) { 588 warning( 589 "%s missing=%s fetched=%s", 590 email.id.to_string(), 591 fields.clear(email.fields).to_string(), 592 fetched_data.to_string() 593 ); 594 continue; 595 } 596 597 email_list.add(email); 598 } catch (Error err) { 599 warning("Unable to convert email for %s %s: %s", 600 uid.to_string(), 601 fetched_data.to_string(), 602 err.message); 603 } 604 } 605 }, cancellable); 606 607 return (email_list.size > 0) ? email_list : null; 608 } 609 610 /** 611 * Returns the sequence numbers for a set of UIDs. 612 * 613 * The `msg_set` parameter must be a set containing UIDs. An error 614 * is thrown if the sequence numbers cannot be determined. 615 */ 616 public async Gee.Map<UID, SequenceNumber> uid_to_position_async(MessageSet msg_set, 617 Cancellable? cancellable) 618 throws Error { 619 if (!msg_set.is_uid) { 620 throw new ImapError.NOT_SUPPORTED("Message set must contain UIDs"); 621 } 622 623 Gee.List<Command> cmds = new Gee.ArrayList<Command>(); 624 cmds.add( 625 new FetchCommand.data_type( 626 msg_set, FetchDataSpecifier.UID, cancellable 627 ) 628 ); 629 630 Gee.HashMap<SequenceNumber, FetchedData> fetched = 631 new Gee.HashMap<SequenceNumber, FetchedData>(); 632 yield exec_commands_async(cmds, fetched, null, cancellable); 633 634 if (fetched.is_empty) { 635 throw new ImapError.INVALID("Server returned no sequence numbers"); 636 } 637 638 Gee.Map<UID,SequenceNumber> map = new Gee.HashMap<UID,SequenceNumber>(); 639 foreach (SequenceNumber seq_num in fetched.keys) { 640 map.set( 641 (UID) fetched.get(seq_num).data_map.get(FetchDataSpecifier.UID), 642 seq_num 643 ); 644 } 645 return map; 646 } 647 648 public async void remove_email_async(Gee.List<MessageSet> msg_sets, 649 GLib.Cancellable? cancellable) 650 throws GLib.Error { 651 ClientSession session = get_session(); 652 Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>(); 653 flags.add(MessageFlag.DELETED); 654 655 Gee.List<Command> cmds = new Gee.ArrayList<Command>(); 656 657 // Build STORE command for all MessageSets, see if all are UIDs so we can use UID EXPUNGE 658 bool all_uid = true; 659 foreach (MessageSet msg_set in msg_sets) { 660 if (!msg_set.is_uid) 661 all_uid = false; 662 663 cmds.add( 664 new StoreCommand(msg_set, ADD_FLAGS, SILENT, flags, cancellable) 665 ); 666 } 667 668 // TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work 669 // for us). See: 670 // http://redmine.yorba.org/issues/7532 671 // 672 // However, current client implementation doesn't properly close INBOX when application 673 // shuts down, which means deleted messages return at application start. See: 674 // http://redmine.yorba.org/issues/6865 675 if (all_uid && session.capabilities.supports_uidplus()) { 676 foreach (MessageSet msg_set in msg_sets) { 677 cmds.add(new ExpungeCommand.uid(msg_set, cancellable)); 678 } 679 } else { 680 cmds.add(new ExpungeCommand(cancellable)); 681 } 682 683 yield exec_commands_async(cmds, null, null, cancellable); 684 } 685 686 public async void mark_email_async(Gee.List<MessageSet> msg_sets, Geary.EmailFlags? flags_to_add, 687 Geary.EmailFlags? flags_to_remove, Cancellable? cancellable) throws Error { 688 Gee.List<MessageFlag> msg_flags_add = new Gee.ArrayList<MessageFlag>(); 689 Gee.List<MessageFlag> msg_flags_remove = new Gee.ArrayList<MessageFlag>(); 690 MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add, 691 out msg_flags_remove); 692 693 if (msg_flags_add.size == 0 && msg_flags_remove.size == 0) 694 return; 695 696 Gee.Collection<Command> cmds = new Gee.ArrayList<Command>(); 697 foreach (MessageSet msg_set in msg_sets) { 698 if (msg_flags_add.size > 0) { 699 cmds.add( 700 new StoreCommand( 701 msg_set, 702 ADD_FLAGS, 703 SILENT, 704 msg_flags_add, 705 cancellable 706 ) 707 ); 708 } 709 710 if (msg_flags_remove.size > 0) { 711 cmds.add( 712 new StoreCommand( 713 msg_set, 714 REMOVE_FLAGS, 715 SILENT, 716 msg_flags_remove, 717 cancellable 718 ) 719 ); 720 } 721 } 722 723 yield exec_commands_async(cmds, null, null, cancellable); 724 } 725 726 // Returns a mapping of the source UID to the destination UID. If the MessageSet is not for 727 // UIDs, then null is returned. If the server doesn't support COPYUID, null is returned. 728 public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set, 729 FolderPath destination, 730 GLib.Cancellable? cancellable) 731 throws GLib.Error { 732 ClientSession session = get_session(); 733 734 MailboxSpecifier mailbox = session.get_mailbox_for_path(destination); 735 CopyCommand cmd = new CopyCommand(msg_set, mailbox, cancellable); 736 737 Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async( 738 Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable); 739 740 if (!responses.has_key(cmd)) 741 return null; 742 743 StatusResponse response = responses.get(cmd); 744 if (response.response_code != null && msg_set.is_uid) { 745 Gee.List<UID>? src_uids = null; 746 Gee.List<UID>? dst_uids = null; 747 try { 748 response.response_code.get_copyuid(null, out src_uids, out dst_uids); 749 } catch (ImapError ierr) { 750 warning("Unable to retrieve COPYUID UIDs: %s", ierr.message); 751 } 752 753 if (src_uids != null && !src_uids.is_empty && 754 dst_uids != null && !dst_uids.is_empty) { 755 Gee.Map<UID, UID> copyuids = new Gee.HashMap<UID, UID>(); 756 int ctr = 0; 757 for (;;) { 758 UID? src_uid = (ctr < src_uids.size) ? src_uids[ctr] : null; 759 UID? dst_uid = (ctr < dst_uids.size) ? dst_uids[ctr] : null; 760 761 if (src_uid != null && dst_uid != null) 762 copyuids.set(src_uid, dst_uid); 763 else 764 break; 765 766 ctr++; 767 } 768 769 if (copyuids.size > 0) 770 return copyuids; 771 } 772 } 773 774 return null; 775 } 776 777 public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria, 778 GLib.Cancellable? cancellable) 779 throws GLib.Error { 780 // always perform a UID SEARCH 781 Gee.Collection<Command> cmds = new Gee.ArrayList<Command>(); 782 cmds.add(new SearchCommand.uid(criteria, cancellable)); 783 784 Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>(); 785 yield exec_commands_async(cmds, null, search_results, cancellable); 786 787 Gee.SortedSet<Imap.UID> tree = null; 788 if (search_results.size > 0) { 789 tree = new Gee.TreeSet<Imap.UID>(); 790 tree.add_all(search_results); 791 } 792 return tree; 793 } 794 795 // NOTE: If fields are added or removed from this method, BASIC_FETCH_FIELDS *must* be updated 796 // as well 797 private void fields_to_fetch_data_types(Geary.Email.Field fields, 798 Gee.List<FetchDataSpecifier> basic_types, 799 Gee.List<string> header_fields) { 800 // The assumption here is that because ENVELOPE is such a common fetch command, the 801 // server will have optimizations for it, whereas if we called for each header in the 802 // envelope separately, the server has to chunk harder parsing the RFC822 header ... have 803 // to add References because IMAP ENVELOPE doesn't return them for some reason (but does 804 // return Message-ID and In-Reply-To) 805 if (fields.is_all_set(Geary.Email.Field.ENVELOPE)) { 806 basic_types.add(FetchDataSpecifier.ENVELOPE); 807 header_fields.add("References"); 808 809 // remove those flags and process any remaining 810 fields = fields.clear(Geary.Email.Field.ENVELOPE); 811 } 812 813 foreach (Geary.Email.Field field in Geary.Email.Field.all()) { 814 switch (fields & field) { 815 case Geary.Email.Field.DATE: 816 header_fields.add("Date"); 817 break; 818 819 case Geary.Email.Field.ORIGINATORS: 820 header_fields.add("From"); 821 header_fields.add("Sender"); 822 header_fields.add("Reply-To"); 823 break; 824 825 case Geary.Email.Field.RECEIVERS: 826 header_fields.add("To"); 827 header_fields.add("Cc"); 828 header_fields.add("Bcc"); 829 break; 830 831 case Geary.Email.Field.REFERENCES: 832 header_fields.add("References"); 833 header_fields.add("Message-ID"); 834 header_fields.add("In-Reply-To"); 835 break; 836 837 case Geary.Email.Field.SUBJECT: 838 header_fields.add("Subject"); 839 break; 840 841 case Geary.Email.Field.HEADER: 842 // TODO: If the entire header is being pulled, then no need to pull down partial 843 // headers; simply get them all and decode what is needed directly 844 basic_types.add(FetchDataSpecifier.RFC822_HEADER); 845 break; 846 847 case Geary.Email.Field.NONE: 848 case Geary.Email.Field.BODY: 849 case Geary.Email.Field.PROPERTIES: 850 case Geary.Email.Field.FLAGS: 851 case Geary.Email.Field.PREVIEW: 852 // not set or fetched separately 853 break; 854 855 default: 856 assert_not_reached(); 857 } 858 } 859 } 860 861 private Geary.Email fetched_data_to_email( 862 UID uid, 863 FetchedData fetched_data, 864 Geary.Email.Field required_fields, 865 FetchBodyDataSpecifier[]? header_specifiers, 866 FetchBodyDataSpecifier? body_specifier, 867 FetchBodyDataSpecifier? preview_specifier, 868 FetchBodyDataSpecifier? preview_charset_specifier 869 ) throws GLib.Error { 870 // note the use of INVALID_ROWID, as the rowid for this email (if one is present in the 871 // database) is unknown at this time; this means ImapDB *must* create a new EmailIdentifier 872 // for this email after create/merge is completed 873 Geary.Email email = new Geary.Email(new ImapDB.EmailIdentifier.no_message_id(uid)); 874 875 // accumulate these to submit Imap.EmailProperties all at once 876 InternalDate? internaldate = null; 877 RFC822Size? rfc822_size = null; 878 879 // accumulate these to submit References all at once 880 RFC822.MessageID? message_id = null; 881 RFC822.MessageIDList? in_reply_to = null; 882 RFC822.MessageIDList? references = null; 883 884 // loop through all available FetchDataTypes and gather converted data 885 foreach (FetchDataSpecifier data_type in fetched_data.data_map.keys) { 886 MessageData? data = fetched_data.data_map.get(data_type); 887 if (data == null) 888 continue; 889 890 switch (data_type) { 891 case FetchDataSpecifier.ENVELOPE: 892 Envelope envelope = (Envelope) data; 893 894 email.set_send_date(envelope.sent); 895 email.set_message_subject(envelope.subject); 896 email.set_originators( 897 envelope.from, 898 envelope.sender.equal_to(envelope.from) || envelope.sender.size == 0 ? null : envelope.sender[0], 899 envelope.reply_to.equal_to(envelope.from) ? null : envelope.reply_to 900 ); 901 email.set_receivers(envelope.to, envelope.cc, envelope.bcc); 902 903 // store these to add to References all at once 904 message_id = envelope.message_id; 905 in_reply_to = envelope.in_reply_to; 906 break; 907 908 case FetchDataSpecifier.RFC822_HEADER: 909 email.set_message_header((RFC822.Header) data); 910 break; 911 912 case FetchDataSpecifier.RFC822_TEXT: 913 email.set_message_body((RFC822.Text) data); 914 break; 915 916 case FetchDataSpecifier.RFC822_SIZE: 917 rfc822_size = (RFC822Size) data; 918 break; 919 920 case FetchDataSpecifier.FLAGS: 921 email.set_flags(new Imap.EmailFlags((MessageFlags) data)); 922 break; 923 924 case FetchDataSpecifier.INTERNALDATE: 925 internaldate = (InternalDate) data; 926 break; 927 928 default: 929 // everything else dropped on the floor (not applicable to Geary.Email) 930 break; 931 } 932 } 933 934 // Only set PROPERTIES if all have been found 935 if (internaldate != null && rfc822_size != null) 936 email.set_email_properties(new Geary.Imap.EmailProperties(internaldate, rfc822_size)); 937 938 // if any headers were requested, convert its fields now 939 if (header_specifiers != null) { 940 // Header fields are case insensitive, so use a 941 // case-insensitive map. 942 // 943 // XXX this is bogus because it doesn't take into the 944 // presence of multiple headers. It's not common, but it's 945 // possible for there to be two To headers, for example 946 Gee.Map<string,string> headers = new Gee.HashMap<string,string>( 947 String.stri_hash, String.stri_equal 948 ); 949 foreach (FetchBodyDataSpecifier header_specifier in header_specifiers) { 950 Memory.Buffer fetched_headers = 951 fetched_data.body_data_map.get(header_specifier); 952 if (fetched_headers != null) { 953 RFC822.Header parsed_headers = new RFC822.Header(fetched_headers); 954 foreach (string name in parsed_headers.get_header_names()) { 955 headers.set(name, parsed_headers.get_raw_header(name)); 956 } 957 } else { 958 warning( 959 "No header specifier \"%s\" found in response:", 960 header_specifier.to_string() 961 ); 962 foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys) { 963 warning(" - has %s", specifier.to_string()); 964 } 965 } 966 } 967 968 // When setting email properties below, the relevant 969 // Geary.Email setter needs to be called regardless of 970 // whether the value being set is null, since the setter 971 // will update the email's flags so we know the email has 972 // the field set and it is null. 973 974 // DATE 975 if (required_but_not_set(DATE, required_fields, email)) { 976 email.set_send_date(unflatten_date(headers.get("Date"))); 977 } 978 979 // ORIGINATORS 980 if (required_but_not_set(ORIGINATORS, required_fields, email)) { 981 // Allow sender to be a list (contra to the RFC), but 982 // only take the first from it 983 RFC822.MailboxAddresses? sender = unflatten_addresses( 984 headers.get("Sender") 985 ); 986 email.set_originators( 987 unflatten_addresses(headers.get("From")), 988 (sender != null && !sender.is_empty) ? sender.get(0) : null, 989 unflatten_addresses(headers.get("Reply-To")) 990 ); 991 } 992 993 // RECEIVERS 994 if (required_but_not_set(RECEIVERS, required_fields, email)) { 995 email.set_receivers( 996 unflatten_addresses(headers.get("To")), 997 unflatten_addresses(headers.get("Cc")), 998 unflatten_addresses(headers.get("Bcc")) 999 ); 1000 } 1001 1002 // REFERENCES 1003 // (Note that it's possible the request used an IMAP ENVELOPE, in which case only the 1004 // References header will be present if REFERENCES were required, which is why 1005 // REFERENCES is set at the bottom of the method, when all information has been gathered 1006 if (message_id == null) { 1007 message_id = unflatten_message_id( 1008 headers.get("Message-ID") 1009 ); 1010 } 1011 if (in_reply_to == null) { 1012 in_reply_to = unflatten_message_id_list( 1013 headers.get("In-Reply-To") 1014 ); 1015 } 1016 if (references == null) { 1017 references = unflatten_message_id_list( 1018 headers.get("References") 1019 ); 1020 } 1021 1022 // SUBJECT 1023 if (required_but_not_set(Geary.Email.Field.SUBJECT, required_fields, email)) { 1024 RFC822.Subject? subject = null; 1025 string? value = headers.get("Subject"); 1026 if (value != null) { 1027 subject = new RFC822.Subject.from_rfc822_string(value); 1028 } 1029 email.set_message_subject(subject); 1030 } 1031 } 1032 1033 // It's possible for all these fields to be null even though they were requested from 1034 // the server, so use requested fields for determination 1035 if (required_but_not_set(Geary.Email.Field.REFERENCES, required_fields, email)) 1036 email.set_full_references(message_id, in_reply_to, references); 1037 1038 // if preview was requested, get it now ... both identifiers 1039 // must be supplied if one is 1040 if (preview_specifier != null || preview_charset_specifier != null) { 1041 Memory.Buffer? preview_headers = fetched_data.body_data_map.get( 1042 preview_charset_specifier 1043 ); 1044 Memory.Buffer? preview_body = fetched_data.body_data_map.get( 1045 preview_specifier 1046 ); 1047 1048 RFC822.PreviewText preview = new RFC822.PreviewText(new Memory.StringBuffer("")); 1049 if (preview_headers != null && preview_headers.size > 0 && 1050 preview_body != null && preview_body.size > 0) { 1051 preview = new RFC822.PreviewText.with_header( 1052 preview_headers, preview_body 1053 ); 1054 } else { 1055 warning("No preview specifiers \"%s\" and \"%s\" found", 1056 preview_specifier.to_string(), preview_charset_specifier.to_string()); 1057 foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys) 1058 warning(" - has %s", specifier.to_string()); 1059 } 1060 email.set_message_preview(preview); 1061 } 1062 1063 // If body was requested, get it now. We also set the preview 1064 // here from the body if possible since for HTML messages at 1065 // least there's a lot of boilerplate HTML to wade through to 1066 // get some actual preview text, which usually requires more 1067 // than Geary.Email.MAX_PREVIEW_BYTES will allow for 1068 if (body_specifier != null) { 1069 if (fetched_data.body_data_map.has_key(body_specifier)) { 1070 email.set_message_body(new Geary.RFC822.Text( 1071 fetched_data.body_data_map.get(body_specifier))); 1072 1073 // Try to set the preview 1074 Geary.RFC822.Message? message = null; 1075 try { 1076 message = email.get_message(); 1077 } catch (EngineError.INCOMPLETE_MESSAGE err) { 1078 debug("Not enough fields to construct message for preview: %s", err.message); 1079 } catch (GLib.Error err) { 1080 warning("Error constructing message for preview: %s", err.message); 1081 } 1082 if (message != null) { 1083 string preview = message.get_preview(); 1084 if (preview.length > Geary.Email.MAX_PREVIEW_BYTES) { 1085 preview = Geary.String.safe_byte_substring( 1086 preview, Geary.Email.MAX_PREVIEW_BYTES 1087 ); 1088 } 1089 email.set_message_preview( 1090 new RFC822.PreviewText.from_string(preview) 1091 ); 1092 } 1093 } else { 1094 warning("No body specifier \"%s\" found", 1095 body_specifier.to_string()); 1096 foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys) 1097 warning(" - has %s", specifier.to_string()); 1098 } 1099 } 1100 1101 return email; 1102 } 1103 1104 /** 1105 * Stores a new message in the remote mailbox. 1106 * 1107 * Returns a no-message-id ImapDB.EmailIdentifier with the UID 1108 * stored in it. 1109 * 1110 * This method does not take a cancellable; there is currently no 1111 * way to tell if an email was created or not if {@link 1112 * exec_commands_async} is cancelled during the append. For 1113 * atomicity's sake, callers need to remove the returned email ID 1114 * if a cancel occurred. 1115 */ 1116 public async Geary.EmailIdentifier? create_email_async(RFC822.Message message, 1117 Geary.EmailFlags? flags, 1118 GLib.DateTime? date_received) 1119 throws GLib.Error { 1120 MessageFlags? msg_flags = null; 1121 if (flags != null) { 1122 Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags); 1123 msg_flags = imap_flags.message_flags; 1124 } else { 1125 msg_flags = new MessageFlags(Geary.iterate<MessageFlag>(MessageFlag.SEEN).to_array_list()); 1126 } 1127 1128 InternalDate? internaldate = null; 1129 if (date_received != null) 1130 internaldate = new InternalDate.from_date_time(date_received); 1131 1132 AppendCommand cmd = new AppendCommand( 1133 this.mailbox, 1134 msg_flags, 1135 internaldate, 1136 message.get_rfc822_buffer(), 1137 null 1138 ); 1139 1140 Gee.Map<Command, StatusResponse> responses = yield exec_commands_async( 1141 Geary.iterate<AppendCommand>(cmd).to_array_list(), null, null, null 1142 ); 1143 1144 // Grab the response and parse out the UID, if available. 1145 StatusResponse response = responses.get(cmd); 1146 if (response.status == Status.OK && response.response_code != null && 1147 response.response_code.get_response_code_type().is_value("appenduid")) { 1148 UID new_id = new UID.checked(response.response_code.get_as_string(2).as_int64()); 1149 1150 return new ImapDB.EmailIdentifier.no_message_id(new_id); 1151 } 1152 1153 // We didn't get a UID back from the server. 1154 return null; 1155 } 1156 1157 /** {@inheritDoc} */ 1158 public override Logging.State to_logging_state() { 1159 return new Logging.State( 1160 this, 1161 "%s, %s, ro: %s, permanent_flags: %s, accepts_user_flags: %s", 1162 base.to_logging_state().format_message(), // XXX this is cruddy 1163 this.folder.to_string(), 1164 this.readonly.to_string(), 1165 this.permanent_flags != null 1166 ? this.permanent_flags.to_string() : "(none)", 1167 this.accepts_user_flags.to_string() 1168 ); 1169 } 1170 1171 /** 1172 * Returns a valid IMAP client session for use by this object. 1173 * 1174 * In addition to the checks made by {@link 1175 * SessionObject.get_session}, this method also ensures that the 1176 * IMAP session is in the SELECTED state for the correct mailbox. 1177 */ 1178 protected override ClientSession get_session() 1179 throws ImapError { 1180 var session = base.get_session(); 1181 if (session.protocol_state != SELECTED && 1182 !this.mailbox.equal_to(session.selected_mailbox)) { 1183 throw new ImapError.NOT_CONNECTED( 1184 "IMAP object no longer SELECTED for %s", 1185 this.mailbox.to_string() 1186 ); 1187 } 1188 return session; 1189 } 1190 1191 // HACK: See https://bugzilla.gnome.org/show_bug.cgi?id=714902 1192 // 1193 // Detect when a server has returned a BAD response to FETCH 1194 // BODY[HEADER.FIELDS (HEADER-LIST)] due to space between 1195 // HEADER.FIELDS and (HEADER-LIST) 1196 private bool retry_bad_header_fields_response(Gee.Collection<FetchCommand> cmds) { 1197 foreach (FetchCommand fetch in cmds) { 1198 if (fetch.status.status == BAD) { 1199 foreach (FetchBodyDataSpecifier specifier in 1200 fetch.for_body_data_specifiers) { 1201 if (specifier.section_part == HEADER_FIELDS || 1202 specifier.section_part == HEADER_FIELDS_NOT) { 1203 // Check the specifier's use of the space, not the 1204 // folder's property, as it's possible the 1205 // property was enabled after sending command but 1206 // before response returned 1207 if (specifier.request_header_fields_space) { 1208 this.quirks.fetch_header_part_no_space = true; 1209 return true; 1210 } 1211 } 1212 } 1213 } 1214 } 1215 1216 return false; 1217 } 1218 1219 private void throw_on_not_ok(StatusResponse response, string cmd) 1220 throws ImapError { 1221 switch (response.status) { 1222 case Status.OK: 1223 // All good 1224 break; 1225 1226 case Status.NO: 1227 throw new ImapError.NOT_SUPPORTED( 1228 "Request %s failed: %s", cmd.to_string(), response.to_string() 1229 ); 1230 1231 default: 1232 throw new ImapError.SERVER_ERROR( 1233 "Unknown response status to %s: %s", 1234 cmd.to_string(), response.to_string() 1235 ); 1236 } 1237 } 1238 1239 private static bool required_but_not_set(Geary.Email.Field check, Geary.Email.Field users_fields, Geary.Email email) { 1240 return users_fields.require(check) ? !email.fields.is_all_set(check) : false; 1241 } 1242 1243 private RFC822.Date? unflatten_date(string? str) { 1244 RFC822.Date? date = null; 1245 if (!String.is_empty_or_whitespace(str)) { 1246 try { 1247 date = new RFC822.Date.from_rfc822_string(str); 1248 } catch (RFC822.Error err) { 1249 // There's not much we can do here aside from logging 1250 // the error, since a lot of email just contain 1251 // invalid addresses 1252 debug("Invalid RFC822 date \"%s\": %s", str, err.message); 1253 } 1254 } 1255 return date; 1256 } 1257 1258 private RFC822.MailboxAddresses? unflatten_addresses(string? str) { 1259 RFC822.MailboxAddresses? addresses = null; 1260 if (!String.is_empty_or_whitespace(str)) { 1261 try { 1262 addresses = new RFC822.MailboxAddresses.from_rfc822_string(str); 1263 } catch (RFC822.Error err) { 1264 // There's not much we can do here aside from logging 1265 // the error, since a lot of email just contain 1266 // invalid addresses 1267 debug("Invalid RFC822 mailbox addresses \"%s\": %s", str, err.message); 1268 } 1269 } 1270 return addresses; 1271 } 1272 1273 private RFC822.MessageID? unflatten_message_id(string? str) { 1274 RFC822.MessageID? id = null; 1275 if (!String.is_empty_or_whitespace(str)) { 1276 try { 1277 id = new RFC822.MessageID.from_rfc822_string(str); 1278 } catch (RFC822.Error err) { 1279 // There's not much we can do here aside from logging 1280 // the error, since a lot of email just contain 1281 // invalid addresses 1282 debug("Invalid RFC822 message id \"%s\": %s", str, err.message); 1283 } 1284 } 1285 return id; 1286 } 1287 1288 private RFC822.MessageIDList? unflatten_message_id_list(string? str) { 1289 RFC822.MessageIDList? ids = null; 1290 if (!String.is_empty_or_whitespace(str)) { 1291 try { 1292 ids = new RFC822.MessageIDList.from_rfc822_string(str); 1293 } catch (RFC822.Error err) { 1294 // There's not much we can do here aside from logging 1295 // the error, since a lot of email just contain 1296 // invalid addresses 1297 debug("Invalid RFC822 message id \"%s\": %s", str, err.message); 1298 } 1299 } 1300 return ids; 1301 } 1302 1303} 1304