1/* 2 * Copyright © 2016 Software Freedom Conservancy Inc. 3 * Copyright © 2020-2021 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 * A base interface for objects that represent decoded RFC822 headers. 11 * 12 * The value of these objects is the decoded form of the header 13 * data. Encoded forms can be obtained via {@link to_rfc822_string}. 14 */ 15public interface Geary.RFC822.DecodedMessageData : 16 Geary.MessageData.AbstractMessageData { 17 18 /** Returns an RFC822-safe string representation of the data. */ 19 public abstract string to_rfc822_string(); 20 21} 22 23/** 24 * A base interface for objects that represent encoded RFC822 header data. 25 * 26 * The value of these objects is the RFC822 encoded form of the header 27 * data. Decoded forms can be obtained via means specific to 28 * implementations of this interface. 29 */ 30public interface Geary.RFC822.EncodedMessageData : 31 Geary.MessageData.BlockMessageData { 32 33} 34 35/** 36 * A RFC822 Message-ID. 37 * 38 * The decoded form of the id is the `addr-spec` portion, that is, 39 * without the leading `<` and tailing `>`. 40 */ 41public class Geary.RFC822.MessageID : 42 Geary.MessageData.StringMessageData, DecodedMessageData { 43 44 public MessageID(string value) { 45 base(value); 46 } 47 48 public MessageID.from_rfc822_string(string rfc822) throws Error { 49 int len = rfc822.length; 50 int start = 0; 51 while (start < len && rfc822[start].isspace()) { 52 start += 1; 53 } 54 char end_delim = 0; 55 bool break_on_space = false; 56 if (start < len) { 57 switch (rfc822[start]) { 58 case '<': 59 // Standard delim 60 start += 1; 61 end_delim = '>'; 62 break; 63 64 case '(': 65 // Non-standard delim 66 start += 1; 67 end_delim = ')'; 68 break; 69 70 default: 71 // no other supported delimiters, so just end at white 72 // space or EOS 73 break_on_space = true; 74 break; 75 } 76 } 77 int end = start + 1; 78 while (end < len && 79 rfc822[end] != end_delim && 80 (!break_on_space || !rfc822[end].isspace())) { 81 end += 1; 82 } 83 84 if (start + 1 >= end) { 85 throw new Error.INVALID("Empty RFC822 message id"); 86 } 87 base(rfc822.slice(start, end)); 88 } 89 90 /** 91 * Returns the {@link Date} in RFC 822 format. 92 */ 93 public string to_rfc822_string() { 94 return "<%s>".printf(this.value); 95 } 96 97} 98 99 100/** 101 * A immutable list of RFC822 Message-ID values. 102 */ 103public class Geary.RFC822.MessageIDList : 104 Geary.MessageData.AbstractMessageData, 105 DecodedMessageData { 106 107 108 /** Returns the number of ids in this list. */ 109 public int size { 110 get { return this.list.size; } 111 } 112 113 /** Determines if there are no ids in the list. */ 114 public bool is_empty { 115 get { return this.list.is_empty; } 116 } 117 118 private Gee.List<MessageID> list = new Gee.ArrayList<MessageID>(); 119 120 121 /** 122 * Constructs a new Message-Id list. 123 * 124 * If the optional collection of ids is not given, the list 125 * is created empty. Otherwise the collection's ids are 126 * added to the list by iterating over it in natural order. 127 */ 128 public MessageIDList(Gee.Collection<MessageID>? collection = null) { 129 if (collection != null) { 130 this.list.add_all(collection); 131 } 132 } 133 134 /** Constructs a new Message-Id list containing a single id. */ 135 public MessageIDList.single(MessageID msg_id){ 136 this(); 137 list.add(msg_id); 138 } 139 140 /** Constructs a new Message-Id list by parsing a RFC822 string. */ 141 public MessageIDList.from_rfc822_string(string rfc822) 142 throws Error { 143 this(); 144 145 // Have seen some mailers use commas between Message-IDs and whitespace inside Message-IDs, 146 // meaning that the standard whitespace tokenizer is not sufficient. The only guarantee 147 // made of a Message-ID is that it's surrounded by angle brackets, so save anything inside 148 // angle brackets 149 // 150 // NOTE: Seen at least one spamfilter mailer that imaginatively uses parens instead of 151 // angle brackets for its Message-IDs; accounting for that as well here. The addt'l logic 152 // is to allow open-parens inside a Message-ID and not treat it as a delimiter; if a 153 // close-parens is found, that's a problem (but isn't expected) 154 // 155 // Also note that this parser will attempt to parse Message-IDs lacking brackets. If one 156 // is found, then it will assume all remaining Message-IDs in the list are bracketed and 157 // be a little less liberal in its parsing. 158 StringBuilder canonicalized = new StringBuilder(); 159 int index = 0; 160 char ch; 161 bool in_message_id = false; 162 bool bracketed = false; 163 while (Ascii.get_next_char(rfc822, ref index, out ch)) { 164 bool add_char = false; 165 switch (ch) { 166 case '<': 167 in_message_id = true; 168 bracketed = true; 169 break; 170 171 case '(': 172 if (!in_message_id) { 173 in_message_id = true; 174 bracketed = true; 175 } else { 176 add_char = true; 177 } 178 break; 179 180 case '>': 181 in_message_id = false; 182 break; 183 184 case ')': 185 if (in_message_id) 186 in_message_id = false; 187 else 188 add_char = true; 189 break; 190 191 default: 192 // deal with Message-IDs without brackets ... bracketed is set to true the 193 // moment the first one is found, so this doesn't deal with combinations of 194 // bracketed and unbracketed text ... MessageID's ctor will deal with adding 195 // brackets to unbracketed id's 196 if (!bracketed) { 197 if (!in_message_id && !ch.isspace()) 198 in_message_id = true; 199 else if (in_message_id && ch.isspace()) 200 in_message_id = false; 201 } 202 203 // only add characters inside the brackets or, if not bracketed, work around 204 add_char = in_message_id; 205 break; 206 } 207 208 if (add_char) 209 canonicalized.append_c(ch); 210 211 if (!in_message_id && !String.is_empty(canonicalized.str)) { 212 list.add(new MessageID(canonicalized.str)); 213 214 canonicalized = new StringBuilder(); 215 } 216 } 217 218 // pick up anything that doesn't end with brackets 219 if (!String.is_empty(canonicalized.str)) 220 list.add(new MessageID(canonicalized.str)); 221 222 if (this.list.is_empty) { 223 throw new Error.INVALID("Empty RFC822 message id list: %s", rfc822); 224 } 225 } 226 227 /** Returns the id at the given index, if it exists. */ 228 public new MessageID? get(int index) { 229 return this.list.get(index); 230 } 231 232 /** Returns a read-only iterator of the ids in this list. */ 233 public Gee.Iterator<MessageID> iterator() { 234 return this.list.read_only_view.iterator(); 235 } 236 237 /** Returns a read-only collection of the ids in this list. */ 238 public Gee.List<MessageID> get_all() { 239 return this.list.read_only_view; 240 } 241 242 /** 243 * Returns a list with the given id appended if not already present. 244 * 245 * This list is returned if the given id is already present, 246 * otherwise the result of a call to {@link concatenate_id} is 247 * returned. 248 */ 249 public MessageIDList merge_id(MessageID other) { 250 return this.list.contains(other) ? this : this.concatenate_id(other); 251 } 252 253 /** 254 * Returns a list with the given ids appended if not already present. 255 * 256 * This list is returned if all given ids are already present, 257 * otherwise the result of a call to {@link concatenate_id} for 258 * each not present is returned. 259 */ 260 public MessageIDList merge_list(MessageIDList other) { 261 var list = this; 262 foreach (var id in other) { 263 if (!this.list.contains(id)) { 264 list = list.concatenate_id(id); 265 } 266 } 267 return list; 268 } 269 270 /** 271 * Returns a new list with the given list appended to this. 272 */ 273 public MessageIDList concatenate_id(MessageID other) { 274 var new_ids = new MessageIDList(this.list); 275 new_ids.list.add(other); 276 return new_ids; 277 } 278 279 /** 280 * Returns a new list with the given list appended to this. 281 */ 282 public MessageIDList concatenate_list(MessageIDList others) { 283 var new_ids = new MessageIDList(this.list); 284 new_ids.list.add_all(others.list); 285 return new_ids; 286 } 287 288 public override string to_string() { 289 return "MessageIDList (%d)".printf(list.size); 290 } 291 292 public string to_rfc822_string() { 293 string[] strings = new string[list.size]; 294 for (int i = 0; i < this.list.size; ++i) { 295 strings[i] = this.list[i].to_rfc822_string(); 296 } 297 298 return string.joinv(" ", strings); 299 } 300 301} 302 303public class Geary.RFC822.Date : 304 Geary.MessageData.AbstractMessageData, 305 Gee.Hashable<Geary.RFC822.Date>, 306 DecodedMessageData { 307 308 309 public GLib.DateTime value { get; private set; } 310 311 private string? rfc822; 312 313 314 public Date(GLib.DateTime datetime) { 315 this.value = datetime; 316 this.rfc822 = null; 317 } 318 319 public Date.from_rfc822_string(string rfc822) throws Error { 320 var date = GMime.utils_header_decode_date(rfc822); 321 if (date == null) { 322 throw new Error.INVALID("Not ISO-8601 date: %s", rfc822); 323 } 324 this.rfc822 = rfc822; 325 this.value = date; 326 } 327 328 /** 329 * Returns the {@link Date} in RFC 822 format. 330 */ 331 public string to_rfc822_string() { 332 if (this.rfc822 == null) { 333 this.rfc822 = GMime.utils_header_format_date(this.value); 334 } 335 return this.rfc822; 336 } 337 338 public virtual bool equal_to(Geary.RFC822.Date other) { 339 return this == other || this.value.equal(other.value); 340 } 341 342 public virtual uint hash() { 343 return this.value.hash(); 344 } 345 346 public override string to_string() { 347 return this.value.to_string(); 348 } 349 350} 351 352public class Geary.RFC822.Subject : 353 Geary.MessageData.StringMessageData, 354 Geary.MessageData.SearchableMessageData, 355 DecodedMessageData { 356 357 public const string REPLY_PREFACE = "Re:"; 358 public const string FORWARD_PREFACE = "Fwd:"; 359 360 361 private string rfc822; 362 363 364 public Subject(string value) { 365 base(value); 366 this.rfc822 = null; 367 } 368 369 public Subject.from_rfc822_string(string rfc822) { 370 base(Utils.decode_rfc822_text_header_value(rfc822)); 371 this.rfc822 = rfc822; 372 } 373 374 /** 375 * Returns the subject line encoded for an RFC 822 message. 376 */ 377 public string to_rfc822_string() { 378 if (this.rfc822 == null) { 379 this.rfc822 = GMime.utils_header_encode_text( 380 get_format_options(), this.value, null 381 ); 382 } 383 return this.rfc822; 384 } 385 386 public bool is_reply() { 387 return value.down().has_prefix(REPLY_PREFACE.down()); 388 } 389 390 public Subject create_reply() { 391 return is_reply() ? new Subject(value) : new Subject("%s %s".printf(REPLY_PREFACE, 392 value)); 393 } 394 395 public bool is_forward() { 396 return value.down().has_prefix(FORWARD_PREFACE.down()); 397 } 398 399 public Subject create_forward() { 400 return is_forward() ? new Subject(value) : new Subject("%s %s".printf(FORWARD_PREFACE, 401 value)); 402 } 403 404 /** 405 * Returns the Subject: line stripped of reply and forwarding prefixes. 406 * 407 * Strips ''all'' prefixes, meaning "Re: Fwd: Soup's on!" will return "Soup's on!" 408 * 409 * Returns an empty string if the Subject: line is empty (or is empty after stripping prefixes). 410 */ 411 public string strip_prefixes() { 412 string subject_base = value; 413 bool changed = false; 414 do { 415 string stripped; 416 try { 417 Regex re_regex = new Regex("^(?i:Re:\\s*)+"); 418 stripped = re_regex.replace(subject_base, -1, 0, ""); 419 420 Regex fwd_regex = new Regex("^(?i:Fwd:\\s*)+"); 421 stripped = fwd_regex.replace(stripped, -1, 0, ""); 422 } catch (RegexError e) { 423 debug("Failed to clean up subject line \"%s\": %s", value, e.message); 424 425 break; 426 } 427 428 changed = (stripped != subject_base); 429 if (changed) 430 subject_base = stripped; 431 } while (changed); 432 433 return String.reduce_whitespace(subject_base); 434 } 435 436 /** 437 * See Geary.MessageData.SearchableMessageData. 438 */ 439 public string to_searchable_string() { 440 return value; 441 } 442 443} 444 445public class Geary.RFC822.Header : 446 Geary.MessageData.BlockMessageData, EncodedMessageData { 447 448 449 private GMime.HeaderList headers; 450 private string[]? names = null; 451 452 453 // The ctors for this class seem the wrong way around, but the 454 // default accepts a memory buffer and not a GMime.HeaderList to 455 // keep it consistent with other EncodedMessageData 456 // implementations. 457 458 public Header(Memory.Buffer buffer) throws Error { 459 base("RFC822.Header", buffer); 460 461 var parser = new GMime.Parser.with_stream( 462 Utils.create_stream_mem(buffer) 463 ); 464 parser.set_respect_content_length(false); 465 parser.set_format(MESSAGE); 466 467 var message = parser.construct_message(null); 468 if (message == null) { 469 throw new Error.INVALID("Unable to parse RFC 822 headers"); 470 } 471 472 this.headers = message.get_header_list(); 473 } 474 475 public Header.from_gmime(GMime.Object gmime) { 476 base( 477 "RFC822.Header", 478 new Memory.StringBuffer(gmime.get_headers(get_format_options())) 479 ); 480 this.headers = gmime.get_header_list(); 481 } 482 483 public string? get_header(string name) { 484 string? value = null; 485 var header = this.headers.get_header(name); 486 if (header != null) { 487 value = header.get_value(); 488 } 489 return value; 490 } 491 492 public string? get_raw_header(string name) { 493 string? value = null; 494 var header = this.headers.get_header(name); 495 if (header != null) { 496 value = header.get_raw_value(); 497 } 498 return value; 499 } 500 501 public string[] get_header_names() { 502 if (this.names == null) { 503 var names = new string[this.headers.get_count()]; 504 for (int i = 0; i < names.length; i++) { 505 names[i] = this.headers.get_header_at(i).get_name(); 506 } 507 this.names = names; 508 } 509 return this.names; 510 } 511 512} 513 514public class Geary.RFC822.Text : 515 Geary.MessageData.BlockMessageData, EncodedMessageData { 516 517 518 private class GMimeBuffer : Memory.Buffer, Memory.UnownedBytesBuffer { 519 520 521 public override size_t allocated_size { 522 get { return (size_t) this.stream.length; } 523 } 524 525 public override size_t size { 526 get { return (size_t) this.stream.length; } 527 } 528 529 private GMime.Stream stream; 530 private GLib.Bytes buf = null; 531 532 public GMimeBuffer(GMime.Stream stream) { 533 this.stream = stream; 534 } 535 536 public override GLib.Bytes get_bytes() { 537 if (this.buf == null) { 538 this.stream.seek(0, SET); 539 uint8[] bytes = new uint8[this.stream.length()]; 540 this.stream.read(bytes); 541 this.buf = new GLib.Bytes.take(bytes); 542 } 543 return this.buf; 544 } 545 546 public unowned uint8[] to_unowned_uint8_array() { 547 return get_bytes().get_data(); 548 } 549 550 } 551 552 public Text(Memory.Buffer buffer) { 553 base("RFC822.Text", buffer); 554 } 555 556 public Text.from_gmime(GMime.Stream gmime) { 557 base("RFC822.Text", new GMimeBuffer(gmime)); 558 } 559 560} 561 562public class Geary.RFC822.Full : 563 Geary.MessageData.BlockMessageData, EncodedMessageData { 564 565 public Full(Memory.Buffer buffer) { 566 base("RFC822.Full", buffer); 567 } 568 569} 570 571/** Represents text providing a preview of an email's body. */ 572public class Geary.RFC822.PreviewText : Geary.RFC822.Text { 573 574 public PreviewText(Memory.Buffer _buffer) { 575 base (_buffer); 576 } 577 578 public PreviewText.with_header(Memory.Buffer preview_header, Memory.Buffer preview) { 579 string preview_text = ""; 580 581 // Parse the header. 582 GMime.Stream header_stream = Utils.create_stream_mem(preview_header); 583 GMime.Parser parser = new GMime.Parser.with_stream(header_stream); 584 GMime.Part? gpart = parser.construct_part(Geary.RFC822.get_parser_options()) as GMime.Part; 585 if (gpart != null) { 586 Part part = new Part(gpart); 587 588 Mime.ContentType content_type = part.content_type; 589 bool is_plain = content_type.is_type("text", "plain"); 590 bool is_html = content_type.is_type("text", "html"); 591 592 if (is_plain || is_html) { 593 // Parse the partial body 594 GMime.DataWrapper body = new GMime.DataWrapper.with_stream( 595 new GMime.StreamMem.with_buffer(preview.get_uint8_array()), 596 gpart.get_content_encoding() 597 ); 598 gpart.set_content(body); 599 600 try { 601 Memory.Buffer preview_buffer = part.write_to_buffer( 602 Part.EncodingConversion.UTF8 603 ); 604 preview_text = Geary.RFC822.Utils.to_preview_text( 605 preview_buffer.get_valid_utf8(), 606 is_html ? TextFormat.HTML : TextFormat.PLAIN 607 ); 608 } catch (Error err) { 609 debug("Failed to parse preview body: %s", err.message); 610 } 611 } 612 } 613 614 base(new Geary.Memory.StringBuffer(preview_text)); 615 } 616 617 public PreviewText.from_string(string preview) { 618 base (new Geary.Memory.StringBuffer(preview)); 619 } 620 621} 622