1 """RFC 2822 message manipulation. 2 3 Note: This is only a very rough sketch of a full RFC-822 parser; in particular 4 the tokenizing of addresses does not adhere to all the quoting rules. 5 6 Note: RFC 2822 is a long awaited update to RFC 822. This module should 7 conform to RFC 2822, and is thus mis-named (it's not worth renaming it). Some 8 effort at RFC 2822 updates have been made, but a thorough audit has not been 9 performed. Consider any RFC 2822 non-conformance to be a bug. 10 11 RFC 2822: http://www.faqs.org/rfcs/rfc2822.html 12 RFC 822 : http://www.faqs.org/rfcs/rfc822.html (obsolete) 13 14 Directions for use: 15 16 To create a Message object: first open a file, e.g.: 17 18 fp = open(file, 'r') 19 20 You can use any other legal way of getting an open file object, e.g. use 21 sys.stdin or call os.popen(). Then pass the open file object to the Message() 22 constructor: 23 24 m = Message(fp) 25 26 This class can work with any input object that supports a readline method. If 27 the input object has seek and tell capability, the rewindbody method will 28 work; also illegal lines will be pushed back onto the input stream. If the 29 input object lacks seek but has an `unread' method that can push back a line 30 of input, Message will use that to push back illegal lines. Thus this class 31 can be used to parse messages coming from a buffered stream. 32 33 The optional `seekable' argument is provided as a workaround for certain stdio 34 libraries in which tell() discards buffered data before discovering that the 35 lseek() system call doesn't work. For maximum portability, you should set the 36 seekable argument to zero to prevent that initial \code{tell} when passing in 37 an unseekable object such as a a file object created from a socket object. If 38 it is 1 on entry -- which it is by default -- the tell() method of the open 39 file object is called once; if this raises an exception, seekable is reset to 40 0. For other nonzero values of seekable, this test is not made. 41 42 To get the text of a particular header there are several methods: 43 44 str = m.getheader(name) 45 str = m.getrawheader(name) 46 47 where name is the name of the header, e.g. 'Subject'. The difference is that 48 getheader() strips the leading and trailing whitespace, while getrawheader() 49 doesn't. Both functions retain embedded whitespace (including newlines) 50 exactly as they are specified in the header, and leave the case of the text 51 unchanged. 52 53 For addresses and address lists there are functions 54 55 realname, mailaddress = m.getaddr(name) 56 list = m.getaddrlist(name) 57 58 where the latter returns a list of (realname, mailaddr) tuples. 59 60 There is also a method 61 62 time = m.getdate(name) 63 64 which parses a Date-like field and returns a time-compatible tuple, 65 i.e. a tuple such as returned by time.localtime() or accepted by 66 time.mktime(). 67 68 See the class definition for lower level access methods. 69 70 There are also some utility functions here. 71 """ 72 # Cleanup and extensions by Eric S. Raymond <esr (at] thyrsus.com> 73 74 import time 75 76 from warnings import warnpy3k 77 warnpy3k("in 3.x, rfc822 has been removed in favor of the email package", 78 stacklevel=2) 79 80 __all__ = ["Message","AddressList","parsedate","parsedate_tz","mktime_tz"] 81 82 _blanklines = ('\r\n', '\n') # Optimization for islast() 83 84 85 class Message: 86 """Represents a single RFC 2822-compliant message.""" 87 88 def __init__(self, fp, seekable = 1): 89 """Initialize the class instance and read the headers.""" 90 if seekable == 1: 91 # Exercise tell() to make sure it works 92 # (and then assume seek() works, too) 93 try: 94 fp.tell() 95 except (AttributeError, IOError): 96 seekable = 0 97 self.fp = fp 98 self.seekable = seekable 99 self.startofheaders = None 100 self.startofbody = None 101 # 102 if self.seekable: 103 try: 104 self.startofheaders = self.fp.tell() 105 except IOError: 106 self.seekable = 0 107 # 108 self.readheaders() 109 # 110 if self.seekable: 111 try: 112 self.startofbody = self.fp.tell() 113 except IOError: 114 self.seekable = 0 115 116 def rewindbody(self): 117 """Rewind the file to the start of the body (if seekable).""" 118 if not self.seekable: 119 raise IOError, "unseekable file" 120 self.fp.seek(self.startofbody) 121 122 def readheaders(self): 123 """Read header lines. 124 125 Read header lines up to the entirely blank line that terminates them. 126 The (normally blank) line that ends the headers is skipped, but not 127 included in the returned list. If a non-header line ends the headers, 128 (which is an error), an attempt is made to backspace over it; it is 129 never included in the returned list. 130 131 The variable self.status is set to the empty string if all went well, 132 otherwise it is an error message. The variable self.headers is a 133 completely uninterpreted list of lines contained in the header (so 134 printing them will reproduce the header exactly as it appears in the 135 file). 136 """ 137 self.dict = {} 138 self.unixfrom = '' 139 self.headers = lst = [] 140 self.status = '' 141 headerseen = "" 142 firstline = 1 143 startofline = unread = tell = None 144 if hasattr(self.fp, 'unread'): 145 unread = self.fp.unread 146 elif self.seekable: 147 tell = self.fp.tell 148 while 1: 149 if tell: 150 try: 151 startofline = tell() 152 except IOError: 153 startofline = tell = None 154 self.seekable = 0 155 line = self.fp.readline() 156 if not line: 157 self.status = 'EOF in headers' 158 break 159 # Skip unix From name time lines 160 if firstline and line.startswith('From '): 161 self.unixfrom = self.unixfrom + line 162 continue 163 firstline = 0 164 if headerseen and line[0] in ' \t': 165 # It's a continuation line. 166 lst.append(line) 167 x = (self.dict[headerseen] + "\n " + line.strip()) 168 self.dict[headerseen] = x.strip() 169 continue 170 elif self.iscomment(line): 171 # It's a comment. Ignore it. 172 continue 173 elif self.islast(line): 174 # Note! No pushback here! The delimiter line gets eaten. 175 break 176 headerseen = self.isheader(line) 177 if headerseen: 178 # It's a legal header line, save it. 179 lst.append(line) 180 self.dict[headerseen] = line[len(headerseen)+1:].strip() 181 continue 182 else: 183 # It's not a header line; throw it back and stop here. 184 if not self.dict: 185 self.status = 'No headers' 186 else: 187 self.status = 'Non-header line where header expected' 188 # Try to undo the read. 189 if unread: 190 unread(line) 191 elif tell: 192 self.fp.seek(startofline) 193 else: 194 self.status = self.status + '; bad seek' 195 break 196 197 def isheader(self, line): 198 """Determine whether a given line is a legal header. 199 200 This method should return the header name, suitably canonicalized. 201 You may override this method in order to use Message parsing on tagged 202 data in RFC 2822-like formats with special header formats. 203 """ 204 i = line.find(':') 205 if i > 0: 206 return line[:i].lower() 207 return None 208 209 def islast(self, line): 210 """Determine whether a line is a legal end of RFC 2822 headers. 211 212 You may override this method if your application wants to bend the 213 rules, e.g. to strip trailing whitespace, or to recognize MH template 214 separators ('--------'). For convenience (e.g. for code reading from 215 sockets) a line consisting of \r\n also matches. 216 """ 217 return line in _blanklines 218 219 def iscomment(self, line): 220 """Determine whether a line should be skipped entirely. 221 222 You may override this method in order to use Message parsing on tagged 223 data in RFC 2822-like formats that support embedded comments or 224 free-text data. 225 """ 226 return False 227 228 def getallmatchingheaders(self, name): 229 """Find all header lines matching a given header name. 230 231 Look through the list of headers and find all lines matching a given 232 header name (and their continuation lines). A list of the lines is 233 returned, without interpretation. If the header does not occur, an 234 empty list is returned. If the header occurs multiple times, all 235 occurrences are returned. Case is not important in the header name. 236 """ 237 name = name.lower() + ':' 238 n = len(name) 239 lst = [] 240 hit = 0 241 for line in self.headers: 242 if line[:n].lower() == name: 243 hit = 1 244 elif not line[:1].isspace(): 245 hit = 0 246 if hit: 247 lst.append(line) 248 return lst 249 250 def getfirstmatchingheader(self, name): 251 """Get the first header line matching name. 252 253 This is similar to getallmatchingheaders, but it returns only the 254 first matching header (and its continuation lines). 255 """ 256 name = name.lower() + ':' 257 n = len(name) 258 lst = [] 259 hit = 0 260 for line in self.headers: 261 if hit: 262 if not line[:1].isspace(): 263 break 264 elif line[:n].lower() == name: 265 hit = 1 266 if hit: 267 lst.append(line) 268 return lst 269 270 def getrawheader(self, name): 271 """A higher-level interface to getfirstmatchingheader(). 272 273 Return a string containing the literal text of the header but with the 274 keyword stripped. All leading, trailing and embedded whitespace is 275 kept in the string, however. Return None if the header does not 276 occur. 277 """ 278 279 lst = self.getfirstmatchingheader(name) 280 if not lst: 281 return None 282 lst[0] = lst[0][len(name) + 1:] 283 return ''.join(lst) 284 285 def getheader(self, name, default=None): 286 """Get the header value for a name. 287 288 This is the normal interface: it returns a stripped version of the 289 header value for a given header name, or None if it doesn't exist. 290 This uses the dictionary version which finds the *last* such header. 291 """ 292 return self.dict.get(name.lower(), default) 293 get = getheader 294 295 def getheaders(self, name): 296 """Get all values for a header. 297 298 This returns a list of values for headers given more than once; each 299 value in the result list is stripped in the same way as the result of 300 getheader(). If the header is not given, return an empty list. 301 """ 302 result = [] 303 current = '' 304 have_header = 0 305 for s in self.getallmatchingheaders(name): 306 if s[0].isspace(): 307 if current: 308 current = "%s\n %s" % (current, s.strip()) 309 else: 310 current = s.strip() 311 else: 312 if have_header: 313 result.append(current) 314 current = s[s.find(":") + 1:].strip() 315 have_header = 1 316 if have_header: 317 result.append(current) 318 return result 319 320 def getaddr(self, name): 321 """Get a single address from a header, as a tuple. 322 323 An example return value: 324 ('Guido van Rossum', 'guido@cwi.nl') 325 """ 326 # New, by Ben Escoto 327 alist = self.getaddrlist(name) 328 if alist: 329 return alist[0] 330 else: 331 return (None, None) 332 333 def getaddrlist(self, name): 334 """Get a list of addresses from a header. 335 336 Retrieves a list of addresses from a header, where each address is a 337 tuple as returned by getaddr(). Scans all named headers, so it works 338 properly with multiple To: or Cc: headers for example. 339 """ 340 raw = [] 341 for h in self.getallmatchingheaders(name): 342 if h[0] in ' \t': 343 raw.append(h) 344 else: 345 if raw: 346 raw.append(', ') 347 i = h.find(':') 348 if i > 0: 349 addr = h[i+1:] 350 raw.append(addr) 351 alladdrs = ''.join(raw) 352 a = AddressList(alladdrs) 353 return a.addresslist 354 355 def getdate(self, name): 356 """Retrieve a date field from a header. 357 358 Retrieves a date field from the named header, returning a tuple 359 compatible with time.mktime(). 360 """ 361 try: 362 data = self[name] 363 except KeyError: 364 return None 365 return parsedate(data) 366 367 def getdate_tz(self, name): 368 """Retrieve a date field from a header as a 10-tuple. 369 370 The first 9 elements make up a tuple compatible with time.mktime(), 371 and the 10th is the offset of the poster's time zone from GMT/UTC. 372 """ 373 try: 374 data = self[name] 375 except KeyError: 376 return None 377 return parsedate_tz(data) 378 379 380 # Access as a dictionary (only finds *last* header of each type): 381 382 def __len__(self): 383 """Get the number of headers in a message.""" 384 return len(self.dict) 385 386 def __getitem__(self, name): 387 """Get a specific header, as from a dictionary.""" 388 return self.dict[name.lower()] 389 390 def __setitem__(self, name, value): 391 """Set the value of a header. 392 393 Note: This is not a perfect inversion of __getitem__, because any 394 changed headers get stuck at the end of the raw-headers list rather 395 than where the altered header was. 396 """ 397 del self[name] # Won't fail if it doesn't exist 398 self.dict[name.lower()] = value 399 text = name + ": " + value 400 for line in text.split("\n"): 401 self.headers.append(line + "\n") 402 403 def __delitem__(self, name): 404 """Delete all occurrences of a specific header, if it is present.""" 405 name = name.lower() 406 if not name in self.dict: 407 return 408 del self.dict[name] 409 name = name + ':' 410 n = len(name) 411 lst = [] 412 hit = 0 413 for i in range(len(self.headers)): 414 line = self.headers[i] 415 if line[:n].lower() == name: 416 hit = 1 417 elif not line[:1].isspace(): 418 hit = 0 419 if hit: 420 lst.append(i) 421 for i in reversed(lst): 422 del self.headers[i] 423 424 def setdefault(self, name, default=""): 425 lowername = name.lower() 426 if lowername in self.dict: 427 return self.dict[lowername] 428 else: 429 text = name + ": " + default 430 for line in text.split("\n"): 431 self.headers.append(line + "\n") 432 self.dict[lowername] = default 433 return default 434 435 def has_key(self, name): 436 """Determine whether a message contains the named header.""" 437 return name.lower() in self.dict 438 439 def __contains__(self, name): 440 """Determine whether a message contains the named header.""" 441 return name.lower() in self.dict 442 443 def __iter__(self): 444 return iter(self.dict) 445 446 def keys(self): 447 """Get all of a message's header field names.""" 448 return self.dict.keys() 449 450 def values(self): 451 """Get all of a message's header field values.""" 452 return self.dict.values() 453 454 def items(self): 455 """Get all of a message's headers. 456 457 Returns a list of name, value tuples. 458 """ 459 return self.dict.items() 460 461 def __str__(self): 462 return ''.join(self.headers) 463 464 465 # Utility functions 466 # ----------------- 467 468 # XXX Should fix unquote() and quote() to be really conformant. 469 # XXX The inverses of the parse functions may also be useful. 470 471 472 def unquote(s): 473 """Remove quotes from a string.""" 474 if len(s) > 1: 475 if s.startswith('"') and s.endswith('"'): 476 return s[1:-1].replace('\\\\', '\\').replace('\\"', '"') 477 if s.startswith('<') and s.endswith('>'): 478 return s[1:-1] 479 return s 480 481 482 def quote(s): 483 """Add quotes around a string.""" 484 return s.replace('\\', '\\\\').replace('"', '\\"') 485 486 487 def parseaddr(address): 488 """Parse an address into a (realname, mailaddr) tuple.""" 489 a = AddressList(address) 490 lst = a.addresslist 491 if not lst: 492 return (None, None) 493 return lst[0] 494 495 496 class AddrlistClass: 497 """Address parser class by Ben Escoto. 498 499 To understand what this class does, it helps to have a copy of 500 RFC 2822 in front of you. 501 502 http://www.faqs.org/rfcs/rfc2822.html 503 504 Note: this class interface is deprecated and may be removed in the future. 505 Use rfc822.AddressList instead. 506 """ 507 508 def __init__(self, field): 509 """Initialize a new instance. 510 511 `field' is an unparsed address header field, containing one or more 512 addresses. 513 """ 514 self.specials = '()<>@,:;.\"[]' 515 self.pos = 0 516 self.LWS = ' \t' 517 self.CR = '\r\n' 518 self.atomends = self.specials + self.LWS + self.CR 519 # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it 520 # is obsolete syntax. RFC 2822 requires that we recognize obsolete 521 # syntax, so allow dots in phrases. 522 self.phraseends = self.atomends.replace('.', '') 523 self.field = field 524 self.commentlist = [] 525 526 def gotonext(self): 527 """Parse up to the start of the next address.""" 528 while self.pos < len(self.field): 529 if self.field[self.pos] in self.LWS + '\n\r': 530 self.pos = self.pos + 1 531 elif self.field[self.pos] == '(': 532 self.commentlist.append(self.getcomment()) 533 else: break 534 535 def getaddrlist(self): 536 """Parse all addresses. 537 538 Returns a list containing all of the addresses. 539 """ 540 result = [] 541 ad = self.getaddress() 542 while ad: 543 result += ad 544 ad = self.getaddress() 545 return result 546 547 def getaddress(self): 548 """Parse the next address.""" 549 self.commentlist = [] 550 self.gotonext() 551 552 oldpos = self.pos 553 oldcl = self.commentlist 554 plist = self.getphraselist() 555 556 self.gotonext() 557 returnlist = [] 558 559 if self.pos >= len(self.field): 560 # Bad email address technically, no domain. 561 if plist: 562 returnlist = [(' '.join(self.commentlist), plist[0])] 563 564 elif self.field[self.pos] in '.@': 565 # email address is just an addrspec 566 # this isn't very efficient since we start over 567 self.pos = oldpos 568 self.commentlist = oldcl 569 addrspec = self.getaddrspec() 570 returnlist = [(' '.join(self.commentlist), addrspec)] 571 572 elif self.field[self.pos] == ':': 573 # address is a group 574 returnlist = [] 575 576 fieldlen = len(self.field) 577 self.pos += 1 578 while self.pos < len(self.field): 579 self.gotonext() 580 if self.pos < fieldlen and self.field[self.pos] == ';': 581 self.pos += 1 582 break 583 returnlist = returnlist + self.getaddress() 584 585 elif self.field[self.pos] == '<': 586 # Address is a phrase then a route addr 587 routeaddr = self.getrouteaddr() 588 589 if self.commentlist: 590 returnlist = [(' '.join(plist) + ' (' + \ 591 ' '.join(self.commentlist) + ')', routeaddr)] 592 else: returnlist = [(' '.join(plist), routeaddr)] 593 594 else: 595 if plist: 596 returnlist = [(' '.join(self.commentlist), plist[0])] 597 elif self.field[self.pos] in self.specials: 598 self.pos += 1 599 600 self.gotonext() 601 if self.pos < len(self.field) and self.field[self.pos] == ',': 602 self.pos += 1 603 return returnlist 604 605 def getrouteaddr(self): 606 """Parse a route address (Return-path value). 607 608 This method just skips all the route stuff and returns the addrspec. 609 """ 610 if self.field[self.pos] != '<': 611 return 612 613 expectroute = 0 614 self.pos += 1 615 self.gotonext() 616 adlist = "" 617 while self.pos < len(self.field): 618 if expectroute: 619 self.getdomain() 620 expectroute = 0 621 elif self.field[self.pos] == '>': 622 self.pos += 1 623 break 624 elif self.field[self.pos] == '@': 625 self.pos += 1 626 expectroute = 1 627 elif self.field[self.pos] == ':': 628 self.pos += 1 629 else: 630 adlist = self.getaddrspec() 631 self.pos += 1 632 break 633 self.gotonext() 634 635 return adlist 636 637 def getaddrspec(self): 638 """Parse an RFC 2822 addr-spec.""" 639 aslist = [] 640 641 self.gotonext() 642 while self.pos < len(self.field): 643 if self.field[self.pos] == '.': 644 aslist.append('.') 645 self.pos += 1 646 elif self.field[self.pos] == '"': 647 aslist.append('"%s"' % self.getquote()) 648 elif self.field[self.pos] in self.atomends: 649 break 650 else: aslist.append(self.getatom()) 651 self.gotonext() 652 653 if self.pos >= len(self.field) or self.field[self.pos] != '@': 654 return ''.join(aslist) 655 656 aslist.append('@') 657 self.pos += 1 658 self.gotonext() 659 return ''.join(aslist) + self.getdomain() 660 661 def getdomain(self): 662 """Get the complete domain name from an address.""" 663 sdlist = [] 664 while self.pos < len(self.field): 665 if self.field[self.pos] in self.LWS: 666 self.pos += 1 667 elif self.field[self.pos] == '(': 668 self.commentlist.append(self.getcomment()) 669 elif self.field[self.pos] == '[': 670 sdlist.append(self.getdomainliteral()) 671 elif self.field[self.pos] == '.': 672 self.pos += 1 673 sdlist.append('.') 674 elif self.field[self.pos] in self.atomends: 675 break 676 else: sdlist.append(self.getatom()) 677 return ''.join(sdlist) 678 679 def getdelimited(self, beginchar, endchars, allowcomments = 1): 680 """Parse a header fragment delimited by special characters. 681 682 `beginchar' is the start character for the fragment. If self is not 683 looking at an instance of `beginchar' then getdelimited returns the 684 empty string. 685 686 `endchars' is a sequence of allowable end-delimiting characters. 687 Parsing stops when one of these is encountered. 688 689 If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed 690 within the parsed fragment. 691 """ 692 if self.field[self.pos] != beginchar: 693 return '' 694 695 slist = [''] 696 quote = 0 697 self.pos += 1 698 while self.pos < len(self.field): 699 if quote == 1: 700 slist.append(self.field[self.pos]) 701 quote = 0 702 elif self.field[self.pos] in endchars: 703 self.pos += 1 704 break 705 elif allowcomments and self.field[self.pos] == '(': 706 slist.append(self.getcomment()) 707 continue # have already advanced pos from getcomment 708 elif self.field[self.pos] == '\\': 709 quote = 1 710 else: 711 slist.append(self.field[self.pos]) 712 self.pos += 1 713 714 return ''.join(slist) 715 716 def getquote(self): 717 """Get a quote-delimited fragment from self's field.""" 718 return self.getdelimited('"', '"\r', 0) 719 720 def getcomment(self): 721 """Get a parenthesis-delimited fragment from self's field.""" 722 return self.getdelimited('(', ')\r', 1) 723 724 def getdomainliteral(self): 725 """Parse an RFC 2822 domain-literal.""" 726 return '[%s]' % self.getdelimited('[', ']\r', 0) 727 728 def getatom(self, atomends=None): 729 """Parse an RFC 2822 atom. 730 731 Optional atomends specifies a different set of end token delimiters 732 (the default is to use self.atomends). This is used e.g. in 733 getphraselist() since phrase endings must not include the `.' (which 734 is legal in phrases).""" 735 atomlist = [''] 736 if atomends is None: 737 atomends = self.atomends 738 739 while self.pos < len(self.field): 740 if self.field[self.pos] in atomends: 741 break 742 else: atomlist.append(self.field[self.pos]) 743 self.pos += 1 744 745 return ''.join(atomlist) 746 747 def getphraselist(self): 748 """Parse a sequence of RFC 2822 phrases. 749 750 A phrase is a sequence of words, which are in turn either RFC 2822 751 atoms or quoted-strings. Phrases are canonicalized by squeezing all 752 runs of continuous whitespace into one space. 753 """ 754 plist = [] 755 756 while self.pos < len(self.field): 757 if self.field[self.pos] in self.LWS: 758 self.pos += 1 759 elif self.field[self.pos] == '"': 760 plist.append(self.getquote()) 761 elif self.field[self.pos] == '(': 762 self.commentlist.append(self.getcomment()) 763 elif self.field[self.pos] in self.phraseends: 764 break 765 else: 766 plist.append(self.getatom(self.phraseends)) 767 768 return plist 769 770 class AddressList(AddrlistClass): 771 """An AddressList encapsulates a list of parsed RFC 2822 addresses.""" 772 def __init__(self, field): 773 AddrlistClass.__init__(self, field) 774 if field: 775 self.addresslist = self.getaddrlist() 776 else: 777 self.addresslist = [] 778 779 def __len__(self): 780 return len(self.addresslist) 781 782 def __str__(self): 783 return ", ".join(map(dump_address_pair, self.addresslist)) 784 785 def __add__(self, other): 786 # Set union 787 newaddr = AddressList(None) 788 newaddr.addresslist = self.addresslist[:] 789 for x in other.addresslist: 790 if not x in self.addresslist: 791 newaddr.addresslist.append(x) 792 return newaddr 793 794 def __iadd__(self, other): 795 # Set union, in-place 796 for x in other.addresslist: 797 if not x in self.addresslist: 798 self.addresslist.append(x) 799 return self 800 801 def __sub__(self, other): 802 # Set difference 803 newaddr = AddressList(None) 804 for x in self.addresslist: 805 if not x in other.addresslist: 806 newaddr.addresslist.append(x) 807 return newaddr 808 809 def __isub__(self, other): 810 # Set difference, in-place 811 for x in other.addresslist: 812 if x in self.addresslist: 813 self.addresslist.remove(x) 814 return self 815 816 def __getitem__(self, index): 817 # Make indexing, slices, and 'in' work 818 return self.addresslist[index] 819 820 def dump_address_pair(pair): 821 """Dump a (name, address) pair in a canonicalized form.""" 822 if pair[0]: 823 return '"' + pair[0] + '" <' + pair[1] + '>' 824 else: 825 return pair[1] 826 827 # Parse a date field 828 829 _monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 830 'aug', 'sep', 'oct', 'nov', 'dec', 831 'january', 'february', 'march', 'april', 'may', 'june', 'july', 832 'august', 'september', 'october', 'november', 'december'] 833 _daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] 834 835 # The timezone table does not include the military time zones defined 836 # in RFC822, other than Z. According to RFC1123, the description in 837 # RFC822 gets the signs wrong, so we can't rely on any such time 838 # zones. RFC1123 recommends that numeric timezone indicators be used 839 # instead of timezone names. 840 841 _timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0, 842 'AST': -400, 'ADT': -300, # Atlantic (used in Canada) 843 'EST': -500, 'EDT': -400, # Eastern 844 'CST': -600, 'CDT': -500, # Central 845 'MST': -700, 'MDT': -600, # Mountain 846 'PST': -800, 'PDT': -700 # Pacific 847 } 848 849 850 def parsedate_tz(data): 851 """Convert a date string to a time tuple. 852 853 Accounts for military timezones. 854 """ 855 if not data: 856 return None 857 data = data.split() 858 if data[0][-1] in (',', '.') or data[0].lower() in _daynames: 859 # There's a dayname here. Skip it 860 del data[0] 861 else: 862 # no space after the "weekday,"? 863 i = data[0].rfind(',') 864 if i >= 0: 865 data[0] = data[0][i+1:] 866 if len(data) == 3: # RFC 850 date, deprecated 867 stuff = data[0].split('-') 868 if len(stuff) == 3: 869 data = stuff + data[1:] 870 if len(data) == 4: 871 s = data[3] 872 i = s.find('+') 873 if i > 0: 874 data[3:] = [s[:i], s[i+1:]] 875 else: 876 data.append('') # Dummy tz 877 if len(data) < 5: 878 return None 879 data = data[:5] 880 [dd, mm, yy, tm, tz] = data 881 mm = mm.lower() 882 if not mm in _monthnames: 883 dd, mm = mm, dd.lower() 884 if not mm in _monthnames: 885 return None 886 mm = _monthnames.index(mm)+1 887 if mm > 12: mm = mm - 12 888 if dd[-1] == ',': 889 dd = dd[:-1] 890 i = yy.find(':') 891 if i > 0: 892 yy, tm = tm, yy 893 if yy[-1] == ',': 894 yy = yy[:-1] 895 if not yy[0].isdigit(): 896 yy, tz = tz, yy 897 if tm[-1] == ',': 898 tm = tm[:-1] 899 tm = tm.split(':') 900 if len(tm) == 2: 901 [thh, tmm] = tm 902 tss = '0' 903 elif len(tm) == 3: 904 [thh, tmm, tss] = tm 905 else: 906 return None 907 try: 908 yy = int(yy) 909 dd = int(dd) 910 thh = int(thh) 911 tmm = int(tmm) 912 tss = int(tss) 913 except ValueError: 914 return None 915 tzoffset = None 916 tz = tz.upper() 917 if tz in _timezones: 918 tzoffset = _timezones[tz] 919 else: 920 try: 921 tzoffset = int(tz) 922 except ValueError: 923 pass 924 # Convert a timezone offset into seconds ; -0500 -> -18000 925 if tzoffset: 926 if tzoffset < 0: 927 tzsign = -1 928 tzoffset = -tzoffset 929 else: 930 tzsign = 1 931 tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60) 932 return (yy, mm, dd, thh, tmm, tss, 0, 1, 0, tzoffset) 933 934 935 def parsedate(data): 936 """Convert a time string to a time tuple.""" 937 t = parsedate_tz(data) 938 if t is None: 939 return t 940 return t[:9] 941 942 943 def mktime_tz(data): 944 """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp.""" 945 if data[9] is None: 946 # No zone info, so localtime is better assumption than GMT 947 return time.mktime(data[:8] + (-1,)) 948 else: 949 t = time.mktime(data[:8] + (0,)) 950 return t - data[9] - time.timezone 951 952 def formatdate(timeval=None): 953 """Returns time format preferred for Internet standards. 954 955 Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 956 957 According to RFC 1123, day and month names must always be in 958 English. If not for that, this code could use strftime(). It 959 can't because strftime() honors the locale and could generated 960 non-English names. 961 """ 962 if timeval is None: 963 timeval = time.time() 964 timeval = time.gmtime(timeval) 965 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 966 ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[timeval[6]], 967 timeval[2], 968 ("Jan", "Feb", "Mar", "Apr", "May", "Jun", 969 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")[timeval[1]-1], 970 timeval[0], timeval[3], timeval[4], timeval[5]) 971 972 973 # When used as script, run a small test program. 974 # The first command line argument must be a filename containing one 975 # message in RFC-822 format. 976 977 if __name__ == '__main__': 978 import sys, os 979 file = os.path.join(os.environ['HOME'], 'Mail/inbox/1') 980 if sys.argv[1:]: file = sys.argv[1] 981 f = open(file, 'r') 982 m = Message(f) 983 print 'From:', m.getaddr('from') 984 print 'To:', m.getaddrlist('to') 985 print 'Subject:', m.getheader('subject') 986 print 'Date:', m.getheader('date') 987 date = m.getdate_tz('date') 988 tz = date[-1] 989 date = time.localtime(mktime_tz(date)) 990 if date: 991 print 'ParsedDate:', time.asctime(date), 992 hhmmss = tz 993 hhmm, ss = divmod(hhmmss, 60) 994 hh, mm = divmod(hhmm, 60) 995 print "%+03d%02d" % (hh, mm), 996 if ss: print ".%02d" % ss, 997 print 998 else: 999 print 'ParsedDate:', None 1000 m.rewindbody() 1001 n = 0 1002 while f.readline(): 1003 n += 1 1004 print 'Lines:', n 1005 print '-'*70 1006 print 'len =', len(m) 1007 if 'Date' in m: print 'Date =', m['Date'] 1008 if 'X-Nonsense' in m: pass 1009 print 'keys =', m.keys() 1010 print 'values =', m.values() 1011 print 'items =', m.items() 1012