_parseaddr.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. # Copyright (C) 2002-2007 Python Software Foundation
  2. # Contact: email-sig@python.org
  3. """Email address parsing code.
  4. Lifted directly from rfc822.py. This should eventually be rewritten.
  5. """
  6. __all__ = [
  7. 'mktime_tz',
  8. 'parsedate',
  9. 'parsedate_tz',
  10. 'quote',
  11. ]
  12. import time, calendar
  13. SPACE = ' '
  14. EMPTYSTRING = ''
  15. COMMASPACE = ', '
  16. # Parse a date field
  17. _monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
  18. 'aug', 'sep', 'oct', 'nov', 'dec',
  19. 'january', 'february', 'march', 'april', 'may', 'june', 'july',
  20. 'august', 'september', 'october', 'november', 'december']
  21. _daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
  22. # The timezone table does not include the military time zones defined
  23. # in RFC822, other than Z. According to RFC1123, the description in
  24. # RFC822 gets the signs wrong, so we can't rely on any such time
  25. # zones. RFC1123 recommends that numeric timezone indicators be used
  26. # instead of timezone names.
  27. _timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0,
  28. 'AST': -400, 'ADT': -300, # Atlantic (used in Canada)
  29. 'EST': -500, 'EDT': -400, # Eastern
  30. 'CST': -600, 'CDT': -500, # Central
  31. 'MST': -700, 'MDT': -600, # Mountain
  32. 'PST': -800, 'PDT': -700 # Pacific
  33. }
  34. def parsedate_tz(data):
  35. """Convert a date string to a time tuple.
  36. Accounts for military timezones.
  37. """
  38. res = _parsedate_tz(data)
  39. if not res:
  40. return
  41. if res[9] is None:
  42. res[9] = 0
  43. return tuple(res)
  44. def _parsedate_tz(data):
  45. """Convert date to extended time tuple.
  46. The last (additional) element is the time zone offset in seconds, except if
  47. the timezone was specified as -0000. In that case the last element is
  48. None. This indicates a UTC timestamp that explicitly declaims knowledge of
  49. the source timezone, as opposed to a +0000 timestamp that indicates the
  50. source timezone really was UTC.
  51. """
  52. if not data:
  53. return
  54. data = data.split()
  55. if not data: # This happens for whitespace-only input.
  56. return None
  57. # The FWS after the comma after the day-of-week is optional, so search and
  58. # adjust for this.
  59. if data[0].endswith(',') or data[0].lower() in _daynames:
  60. # There's a dayname here. Skip it
  61. del data[0]
  62. else:
  63. i = data[0].rfind(',')
  64. if i >= 0:
  65. data[0] = data[0][i+1:]
  66. if len(data) == 3: # RFC 850 date, deprecated
  67. stuff = data[0].split('-')
  68. if len(stuff) == 3:
  69. data = stuff + data[1:]
  70. if len(data) == 4:
  71. s = data[3]
  72. i = s.find('+')
  73. if i == -1:
  74. i = s.find('-')
  75. if i > 0:
  76. data[3:] = [s[:i], s[i:]]
  77. else:
  78. data.append('') # Dummy tz
  79. if len(data) < 5:
  80. return None
  81. data = data[:5]
  82. [dd, mm, yy, tm, tz] = data
  83. mm = mm.lower()
  84. if mm not in _monthnames:
  85. dd, mm = mm, dd.lower()
  86. if mm not in _monthnames:
  87. return None
  88. mm = _monthnames.index(mm) + 1
  89. if mm > 12:
  90. mm -= 12
  91. if dd[-1] == ',':
  92. dd = dd[:-1]
  93. i = yy.find(':')
  94. if i > 0:
  95. yy, tm = tm, yy
  96. if yy[-1] == ',':
  97. yy = yy[:-1]
  98. if not yy[0].isdigit():
  99. yy, tz = tz, yy
  100. if tm[-1] == ',':
  101. tm = tm[:-1]
  102. tm = tm.split(':')
  103. if len(tm) == 2:
  104. [thh, tmm] = tm
  105. tss = '0'
  106. elif len(tm) == 3:
  107. [thh, tmm, tss] = tm
  108. elif len(tm) == 1 and '.' in tm[0]:
  109. # Some non-compliant MUAs use '.' to separate time elements.
  110. tm = tm[0].split('.')
  111. if len(tm) == 2:
  112. [thh, tmm] = tm
  113. tss = 0
  114. elif len(tm) == 3:
  115. [thh, tmm, tss] = tm
  116. else:
  117. return None
  118. else:
  119. return None
  120. try:
  121. yy = int(yy)
  122. dd = int(dd)
  123. thh = int(thh)
  124. tmm = int(tmm)
  125. tss = int(tss)
  126. except ValueError:
  127. return None
  128. # Check for a yy specified in two-digit format, then convert it to the
  129. # appropriate four-digit format, according to the POSIX standard. RFC 822
  130. # calls for a two-digit yy, but RFC 2822 (which obsoletes RFC 822)
  131. # mandates a 4-digit yy. For more information, see the documentation for
  132. # the time module.
  133. if yy < 100:
  134. # The year is between 1969 and 1999 (inclusive).
  135. if yy > 68:
  136. yy += 1900
  137. # The year is between 2000 and 2068 (inclusive).
  138. else:
  139. yy += 2000
  140. tzoffset = None
  141. tz = tz.upper()
  142. if tz in _timezones:
  143. tzoffset = _timezones[tz]
  144. else:
  145. try:
  146. tzoffset = int(tz)
  147. except ValueError:
  148. pass
  149. if tzoffset==0 and tz.startswith('-'):
  150. tzoffset = None
  151. # Convert a timezone offset into seconds ; -0500 -> -18000
  152. if tzoffset:
  153. if tzoffset < 0:
  154. tzsign = -1
  155. tzoffset = -tzoffset
  156. else:
  157. tzsign = 1
  158. tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60)
  159. # Daylight Saving Time flag is set to -1, since DST is unknown.
  160. return [yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset]
  161. def parsedate(data):
  162. """Convert a time string to a time tuple."""
  163. t = parsedate_tz(data)
  164. if isinstance(t, tuple):
  165. return t[:9]
  166. else:
  167. return t
  168. def mktime_tz(data):
  169. """Turn a 10-tuple as returned by parsedate_tz() into a POSIX timestamp."""
  170. if data[9] is None:
  171. # No zone info, so localtime is better assumption than GMT
  172. return time.mktime(data[:8] + (-1,))
  173. else:
  174. t = calendar.timegm(data)
  175. return t - data[9]
  176. def quote(str):
  177. """Prepare string to be used in a quoted string.
  178. Turns backslash and double quote characters into quoted pairs. These
  179. are the only characters that need to be quoted inside a quoted string.
  180. Does not add the surrounding double quotes.
  181. """
  182. return str.replace('\\', '\\\\').replace('"', '\\"')
  183. class AddrlistClass:
  184. """Address parser class by Ben Escoto.
  185. To understand what this class does, it helps to have a copy of RFC 2822 in
  186. front of you.
  187. Note: this class interface is deprecated and may be removed in the future.
  188. Use email.utils.AddressList instead.
  189. """
  190. def __init__(self, field):
  191. """Initialize a new instance.
  192. `field' is an unparsed address header field, containing
  193. one or more addresses.
  194. """
  195. self.specials = '()<>@,:;.\"[]'
  196. self.pos = 0
  197. self.LWS = ' \t'
  198. self.CR = '\r\n'
  199. self.FWS = self.LWS + self.CR
  200. self.atomends = self.specials + self.LWS + self.CR
  201. # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
  202. # is obsolete syntax. RFC 2822 requires that we recognize obsolete
  203. # syntax, so allow dots in phrases.
  204. self.phraseends = self.atomends.replace('.', '')
  205. self.field = field
  206. self.commentlist = []
  207. def gotonext(self):
  208. """Skip white space and extract comments."""
  209. wslist = []
  210. while self.pos < len(self.field):
  211. if self.field[self.pos] in self.LWS + '\n\r':
  212. if self.field[self.pos] not in '\n\r':
  213. wslist.append(self.field[self.pos])
  214. self.pos += 1
  215. elif self.field[self.pos] == '(':
  216. self.commentlist.append(self.getcomment())
  217. else:
  218. break
  219. return EMPTYSTRING.join(wslist)
  220. def getaddrlist(self):
  221. """Parse all addresses.
  222. Returns a list containing all of the addresses.
  223. """
  224. result = []
  225. while self.pos < len(self.field):
  226. ad = self.getaddress()
  227. if ad:
  228. result += ad
  229. else:
  230. result.append(('', ''))
  231. return result
  232. def getaddress(self):
  233. """Parse the next address."""
  234. self.commentlist = []
  235. self.gotonext()
  236. oldpos = self.pos
  237. oldcl = self.commentlist
  238. plist = self.getphraselist()
  239. self.gotonext()
  240. returnlist = []
  241. if self.pos >= len(self.field):
  242. # Bad email address technically, no domain.
  243. if plist:
  244. returnlist = [(SPACE.join(self.commentlist), plist[0])]
  245. elif self.field[self.pos] in '.@':
  246. # email address is just an addrspec
  247. # this isn't very efficient since we start over
  248. self.pos = oldpos
  249. self.commentlist = oldcl
  250. addrspec = self.getaddrspec()
  251. returnlist = [(SPACE.join(self.commentlist), addrspec)]
  252. elif self.field[self.pos] == ':':
  253. # address is a group
  254. returnlist = []
  255. fieldlen = len(self.field)
  256. self.pos += 1
  257. while self.pos < len(self.field):
  258. self.gotonext()
  259. if self.pos < fieldlen and self.field[self.pos] == ';':
  260. self.pos += 1
  261. break
  262. returnlist = returnlist + self.getaddress()
  263. elif self.field[self.pos] == '<':
  264. # Address is a phrase then a route addr
  265. routeaddr = self.getrouteaddr()
  266. if self.commentlist:
  267. returnlist = [(SPACE.join(plist) + ' (' +
  268. ' '.join(self.commentlist) + ')', routeaddr)]
  269. else:
  270. returnlist = [(SPACE.join(plist), routeaddr)]
  271. else:
  272. if plist:
  273. returnlist = [(SPACE.join(self.commentlist), plist[0])]
  274. elif self.field[self.pos] in self.specials:
  275. self.pos += 1
  276. self.gotonext()
  277. if self.pos < len(self.field) and self.field[self.pos] == ',':
  278. self.pos += 1
  279. return returnlist
  280. def getrouteaddr(self):
  281. """Parse a route address (Return-path value).
  282. This method just skips all the route stuff and returns the addrspec.
  283. """
  284. if self.field[self.pos] != '<':
  285. return
  286. expectroute = False
  287. self.pos += 1
  288. self.gotonext()
  289. adlist = ''
  290. while self.pos < len(self.field):
  291. if expectroute:
  292. self.getdomain()
  293. expectroute = False
  294. elif self.field[self.pos] == '>':
  295. self.pos += 1
  296. break
  297. elif self.field[self.pos] == '@':
  298. self.pos += 1
  299. expectroute = True
  300. elif self.field[self.pos] == ':':
  301. self.pos += 1
  302. else:
  303. adlist = self.getaddrspec()
  304. self.pos += 1
  305. break
  306. self.gotonext()
  307. return adlist
  308. def getaddrspec(self):
  309. """Parse an RFC 2822 addr-spec."""
  310. aslist = []
  311. self.gotonext()
  312. while self.pos < len(self.field):
  313. preserve_ws = True
  314. if self.field[self.pos] == '.':
  315. if aslist and not aslist[-1].strip():
  316. aslist.pop()
  317. aslist.append('.')
  318. self.pos += 1
  319. preserve_ws = False
  320. elif self.field[self.pos] == '"':
  321. aslist.append('"%s"' % quote(self.getquote()))
  322. elif self.field[self.pos] in self.atomends:
  323. if aslist and not aslist[-1].strip():
  324. aslist.pop()
  325. break
  326. else:
  327. aslist.append(self.getatom())
  328. ws = self.gotonext()
  329. if preserve_ws and ws:
  330. aslist.append(ws)
  331. if self.pos >= len(self.field) or self.field[self.pos] != '@':
  332. return EMPTYSTRING.join(aslist)
  333. aslist.append('@')
  334. self.pos += 1
  335. self.gotonext()
  336. domain = self.getdomain()
  337. if not domain:
  338. # Invalid domain, return an empty address instead of returning a
  339. # local part to denote failed parsing.
  340. return EMPTYSTRING
  341. return EMPTYSTRING.join(aslist) + domain
  342. def getdomain(self):
  343. """Get the complete domain name from an address."""
  344. sdlist = []
  345. while self.pos < len(self.field):
  346. if self.field[self.pos] in self.LWS:
  347. self.pos += 1
  348. elif self.field[self.pos] == '(':
  349. self.commentlist.append(self.getcomment())
  350. elif self.field[self.pos] == '[':
  351. sdlist.append(self.getdomainliteral())
  352. elif self.field[self.pos] == '.':
  353. self.pos += 1
  354. sdlist.append('.')
  355. elif self.field[self.pos] == '@':
  356. # bpo-34155: Don't parse domains with two `@` like
  357. # `a@malicious.org@important.com`.
  358. return EMPTYSTRING
  359. elif self.field[self.pos] in self.atomends:
  360. break
  361. else:
  362. sdlist.append(self.getatom())
  363. return EMPTYSTRING.join(sdlist)
  364. def getdelimited(self, beginchar, endchars, allowcomments=True):
  365. """Parse a header fragment delimited by special characters.
  366. `beginchar' is the start character for the fragment.
  367. If self is not looking at an instance of `beginchar' then
  368. getdelimited returns the empty string.
  369. `endchars' is a sequence of allowable end-delimiting characters.
  370. Parsing stops when one of these is encountered.
  371. If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
  372. within the parsed fragment.
  373. """
  374. if self.field[self.pos] != beginchar:
  375. return ''
  376. slist = ['']
  377. quote = False
  378. self.pos += 1
  379. while self.pos < len(self.field):
  380. if quote:
  381. slist.append(self.field[self.pos])
  382. quote = False
  383. elif self.field[self.pos] in endchars:
  384. self.pos += 1
  385. break
  386. elif allowcomments and self.field[self.pos] == '(':
  387. slist.append(self.getcomment())
  388. continue # have already advanced pos from getcomment
  389. elif self.field[self.pos] == '\\':
  390. quote = True
  391. else:
  392. slist.append(self.field[self.pos])
  393. self.pos += 1
  394. return EMPTYSTRING.join(slist)
  395. def getquote(self):
  396. """Get a quote-delimited fragment from self's field."""
  397. return self.getdelimited('"', '"\r', False)
  398. def getcomment(self):
  399. """Get a parenthesis-delimited fragment from self's field."""
  400. return self.getdelimited('(', ')\r', True)
  401. def getdomainliteral(self):
  402. """Parse an RFC 2822 domain-literal."""
  403. return '[%s]' % self.getdelimited('[', ']\r', False)
  404. def getatom(self, atomends=None):
  405. """Parse an RFC 2822 atom.
  406. Optional atomends specifies a different set of end token delimiters
  407. (the default is to use self.atomends). This is used e.g. in
  408. getphraselist() since phrase endings must not include the `.' (which
  409. is legal in phrases)."""
  410. atomlist = ['']
  411. if atomends is None:
  412. atomends = self.atomends
  413. while self.pos < len(self.field):
  414. if self.field[self.pos] in atomends:
  415. break
  416. else:
  417. atomlist.append(self.field[self.pos])
  418. self.pos += 1
  419. return EMPTYSTRING.join(atomlist)
  420. def getphraselist(self):
  421. """Parse a sequence of RFC 2822 phrases.
  422. A phrase is a sequence of words, which are in turn either RFC 2822
  423. atoms or quoted-strings. Phrases are canonicalized by squeezing all
  424. runs of continuous whitespace into one space.
  425. """
  426. plist = []
  427. while self.pos < len(self.field):
  428. if self.field[self.pos] in self.FWS:
  429. self.pos += 1
  430. elif self.field[self.pos] == '"':
  431. plist.append(self.getquote())
  432. elif self.field[self.pos] == '(':
  433. self.commentlist.append(self.getcomment())
  434. elif self.field[self.pos] in self.phraseends:
  435. break
  436. else:
  437. plist.append(self.getatom(self.phraseends))
  438. return plist
  439. class AddressList(AddrlistClass):
  440. """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
  441. def __init__(self, field):
  442. AddrlistClass.__init__(self, field)
  443. if field:
  444. self.addresslist = self.getaddrlist()
  445. else:
  446. self.addresslist = []
  447. def __len__(self):
  448. return len(self.addresslist)
  449. def __add__(self, other):
  450. # Set union
  451. newaddr = AddressList(None)
  452. newaddr.addresslist = self.addresslist[:]
  453. for x in other.addresslist:
  454. if not x in self.addresslist:
  455. newaddr.addresslist.append(x)
  456. return newaddr
  457. def __iadd__(self, other):
  458. # Set union, in-place
  459. for x in other.addresslist:
  460. if not x in self.addresslist:
  461. self.addresslist.append(x)
  462. return self
  463. def __sub__(self, other):
  464. # Set difference
  465. newaddr = AddressList(None)
  466. for x in self.addresslist:
  467. if not x in other.addresslist:
  468. newaddr.addresslist.append(x)
  469. return newaddr
  470. def __isub__(self, other):
  471. # Set difference, in-place
  472. for x in other.addresslist:
  473. if x in self.addresslist:
  474. self.addresslist.remove(x)
  475. return self
  476. def __getitem__(self, index):
  477. # Make indexing, slices, and 'in' work
  478. return self.addresslist[index]