_parseaddr.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  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 None
  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. if not (dd and mm and yy):
  84. return None
  85. mm = mm.lower()
  86. if mm not in _monthnames:
  87. dd, mm = mm, dd.lower()
  88. if mm not in _monthnames:
  89. return None
  90. mm = _monthnames.index(mm) + 1
  91. if mm > 12:
  92. mm -= 12
  93. if dd[-1] == ',':
  94. dd = dd[:-1]
  95. i = yy.find(':')
  96. if i > 0:
  97. yy, tm = tm, yy
  98. if yy[-1] == ',':
  99. yy = yy[:-1]
  100. if not yy:
  101. return None
  102. if not yy[0].isdigit():
  103. yy, tz = tz, yy
  104. if tm[-1] == ',':
  105. tm = tm[:-1]
  106. tm = tm.split(':')
  107. if len(tm) == 2:
  108. [thh, tmm] = tm
  109. tss = '0'
  110. elif len(tm) == 3:
  111. [thh, tmm, tss] = tm
  112. elif len(tm) == 1 and '.' in tm[0]:
  113. # Some non-compliant MUAs use '.' to separate time elements.
  114. tm = tm[0].split('.')
  115. if len(tm) == 2:
  116. [thh, tmm] = tm
  117. tss = 0
  118. elif len(tm) == 3:
  119. [thh, tmm, tss] = tm
  120. else:
  121. return None
  122. else:
  123. return None
  124. try:
  125. yy = int(yy)
  126. dd = int(dd)
  127. thh = int(thh)
  128. tmm = int(tmm)
  129. tss = int(tss)
  130. except ValueError:
  131. return None
  132. # Check for a yy specified in two-digit format, then convert it to the
  133. # appropriate four-digit format, according to the POSIX standard. RFC 822
  134. # calls for a two-digit yy, but RFC 2822 (which obsoletes RFC 822)
  135. # mandates a 4-digit yy. For more information, see the documentation for
  136. # the time module.
  137. if yy < 100:
  138. # The year is between 1969 and 1999 (inclusive).
  139. if yy > 68:
  140. yy += 1900
  141. # The year is between 2000 and 2068 (inclusive).
  142. else:
  143. yy += 2000
  144. tzoffset = None
  145. tz = tz.upper()
  146. if tz in _timezones:
  147. tzoffset = _timezones[tz]
  148. else:
  149. try:
  150. tzoffset = int(tz)
  151. except ValueError:
  152. pass
  153. if tzoffset==0 and tz.startswith('-'):
  154. tzoffset = None
  155. # Convert a timezone offset into seconds ; -0500 -> -18000
  156. if tzoffset:
  157. if tzoffset < 0:
  158. tzsign = -1
  159. tzoffset = -tzoffset
  160. else:
  161. tzsign = 1
  162. tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60)
  163. # Daylight Saving Time flag is set to -1, since DST is unknown.
  164. return [yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset]
  165. def parsedate(data):
  166. """Convert a time string to a time tuple."""
  167. t = parsedate_tz(data)
  168. if isinstance(t, tuple):
  169. return t[:9]
  170. else:
  171. return t
  172. def mktime_tz(data):
  173. """Turn a 10-tuple as returned by parsedate_tz() into a POSIX timestamp."""
  174. if data[9] is None:
  175. # No zone info, so localtime is better assumption than GMT
  176. return time.mktime(data[:8] + (-1,))
  177. else:
  178. t = calendar.timegm(data)
  179. return t - data[9]
  180. def quote(str):
  181. """Prepare string to be used in a quoted string.
  182. Turns backslash and double quote characters into quoted pairs. These
  183. are the only characters that need to be quoted inside a quoted string.
  184. Does not add the surrounding double quotes.
  185. """
  186. return str.replace('\\', '\\\\').replace('"', '\\"')
  187. class AddrlistClass:
  188. """Address parser class by Ben Escoto.
  189. To understand what this class does, it helps to have a copy of RFC 2822 in
  190. front of you.
  191. Note: this class interface is deprecated and may be removed in the future.
  192. Use email.utils.AddressList instead.
  193. """
  194. def __init__(self, field):
  195. """Initialize a new instance.
  196. `field' is an unparsed address header field, containing
  197. one or more addresses.
  198. """
  199. self.specials = '()<>@,:;.\"[]'
  200. self.pos = 0
  201. self.LWS = ' \t'
  202. self.CR = '\r\n'
  203. self.FWS = self.LWS + self.CR
  204. self.atomends = self.specials + self.LWS + self.CR
  205. # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
  206. # is obsolete syntax. RFC 2822 requires that we recognize obsolete
  207. # syntax, so allow dots in phrases.
  208. self.phraseends = self.atomends.replace('.', '')
  209. self.field = field
  210. self.commentlist = []
  211. def gotonext(self):
  212. """Skip white space and extract comments."""
  213. wslist = []
  214. while self.pos < len(self.field):
  215. if self.field[self.pos] in self.LWS + '\n\r':
  216. if self.field[self.pos] not in '\n\r':
  217. wslist.append(self.field[self.pos])
  218. self.pos += 1
  219. elif self.field[self.pos] == '(':
  220. self.commentlist.append(self.getcomment())
  221. else:
  222. break
  223. return EMPTYSTRING.join(wslist)
  224. def getaddrlist(self):
  225. """Parse all addresses.
  226. Returns a list containing all of the addresses.
  227. """
  228. result = []
  229. while self.pos < len(self.field):
  230. ad = self.getaddress()
  231. if ad:
  232. result += ad
  233. else:
  234. result.append(('', ''))
  235. return result
  236. def getaddress(self):
  237. """Parse the next address."""
  238. self.commentlist = []
  239. self.gotonext()
  240. oldpos = self.pos
  241. oldcl = self.commentlist
  242. plist = self.getphraselist()
  243. self.gotonext()
  244. returnlist = []
  245. if self.pos >= len(self.field):
  246. # Bad email address technically, no domain.
  247. if plist:
  248. returnlist = [(SPACE.join(self.commentlist), plist[0])]
  249. elif self.field[self.pos] in '.@':
  250. # email address is just an addrspec
  251. # this isn't very efficient since we start over
  252. self.pos = oldpos
  253. self.commentlist = oldcl
  254. addrspec = self.getaddrspec()
  255. returnlist = [(SPACE.join(self.commentlist), addrspec)]
  256. elif self.field[self.pos] == ':':
  257. # address is a group
  258. returnlist = []
  259. fieldlen = len(self.field)
  260. self.pos += 1
  261. while self.pos < len(self.field):
  262. self.gotonext()
  263. if self.pos < fieldlen and self.field[self.pos] == ';':
  264. self.pos += 1
  265. break
  266. returnlist = returnlist + self.getaddress()
  267. elif self.field[self.pos] == '<':
  268. # Address is a phrase then a route addr
  269. routeaddr = self.getrouteaddr()
  270. if self.commentlist:
  271. returnlist = [(SPACE.join(plist) + ' (' +
  272. ' '.join(self.commentlist) + ')', routeaddr)]
  273. else:
  274. returnlist = [(SPACE.join(plist), routeaddr)]
  275. else:
  276. if plist:
  277. returnlist = [(SPACE.join(self.commentlist), plist[0])]
  278. elif self.field[self.pos] in self.specials:
  279. self.pos += 1
  280. self.gotonext()
  281. if self.pos < len(self.field) and self.field[self.pos] == ',':
  282. self.pos += 1
  283. return returnlist
  284. def getrouteaddr(self):
  285. """Parse a route address (Return-path value).
  286. This method just skips all the route stuff and returns the addrspec.
  287. """
  288. if self.field[self.pos] != '<':
  289. return
  290. expectroute = False
  291. self.pos += 1
  292. self.gotonext()
  293. adlist = ''
  294. while self.pos < len(self.field):
  295. if expectroute:
  296. self.getdomain()
  297. expectroute = False
  298. elif self.field[self.pos] == '>':
  299. self.pos += 1
  300. break
  301. elif self.field[self.pos] == '@':
  302. self.pos += 1
  303. expectroute = True
  304. elif self.field[self.pos] == ':':
  305. self.pos += 1
  306. else:
  307. adlist = self.getaddrspec()
  308. self.pos += 1
  309. break
  310. self.gotonext()
  311. return adlist
  312. def getaddrspec(self):
  313. """Parse an RFC 2822 addr-spec."""
  314. aslist = []
  315. self.gotonext()
  316. while self.pos < len(self.field):
  317. preserve_ws = True
  318. if self.field[self.pos] == '.':
  319. if aslist and not aslist[-1].strip():
  320. aslist.pop()
  321. aslist.append('.')
  322. self.pos += 1
  323. preserve_ws = False
  324. elif self.field[self.pos] == '"':
  325. aslist.append('"%s"' % quote(self.getquote()))
  326. elif self.field[self.pos] in self.atomends:
  327. if aslist and not aslist[-1].strip():
  328. aslist.pop()
  329. break
  330. else:
  331. aslist.append(self.getatom())
  332. ws = self.gotonext()
  333. if preserve_ws and ws:
  334. aslist.append(ws)
  335. if self.pos >= len(self.field) or self.field[self.pos] != '@':
  336. return EMPTYSTRING.join(aslist)
  337. aslist.append('@')
  338. self.pos += 1
  339. self.gotonext()
  340. domain = self.getdomain()
  341. if not domain:
  342. # Invalid domain, return an empty address instead of returning a
  343. # local part to denote failed parsing.
  344. return EMPTYSTRING
  345. return EMPTYSTRING.join(aslist) + domain
  346. def getdomain(self):
  347. """Get the complete domain name from an address."""
  348. sdlist = []
  349. while self.pos < len(self.field):
  350. if self.field[self.pos] in self.LWS:
  351. self.pos += 1
  352. elif self.field[self.pos] == '(':
  353. self.commentlist.append(self.getcomment())
  354. elif self.field[self.pos] == '[':
  355. sdlist.append(self.getdomainliteral())
  356. elif self.field[self.pos] == '.':
  357. self.pos += 1
  358. sdlist.append('.')
  359. elif self.field[self.pos] == '@':
  360. # bpo-34155: Don't parse domains with two `@` like
  361. # `a@malicious.org@important.com`.
  362. return EMPTYSTRING
  363. elif self.field[self.pos] in self.atomends:
  364. break
  365. else:
  366. sdlist.append(self.getatom())
  367. return EMPTYSTRING.join(sdlist)
  368. def getdelimited(self, beginchar, endchars, allowcomments=True):
  369. """Parse a header fragment delimited by special characters.
  370. `beginchar' is the start character for the fragment.
  371. If self is not looking at an instance of `beginchar' then
  372. getdelimited returns the empty string.
  373. `endchars' is a sequence of allowable end-delimiting characters.
  374. Parsing stops when one of these is encountered.
  375. If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
  376. within the parsed fragment.
  377. """
  378. if self.field[self.pos] != beginchar:
  379. return ''
  380. slist = ['']
  381. quote = False
  382. self.pos += 1
  383. while self.pos < len(self.field):
  384. if quote:
  385. slist.append(self.field[self.pos])
  386. quote = False
  387. elif self.field[self.pos] in endchars:
  388. self.pos += 1
  389. break
  390. elif allowcomments and self.field[self.pos] == '(':
  391. slist.append(self.getcomment())
  392. continue # have already advanced pos from getcomment
  393. elif self.field[self.pos] == '\\':
  394. quote = True
  395. else:
  396. slist.append(self.field[self.pos])
  397. self.pos += 1
  398. return EMPTYSTRING.join(slist)
  399. def getquote(self):
  400. """Get a quote-delimited fragment from self's field."""
  401. return self.getdelimited('"', '"\r', False)
  402. def getcomment(self):
  403. """Get a parenthesis-delimited fragment from self's field."""
  404. return self.getdelimited('(', ')\r', True)
  405. def getdomainliteral(self):
  406. """Parse an RFC 2822 domain-literal."""
  407. return '[%s]' % self.getdelimited('[', ']\r', False)
  408. def getatom(self, atomends=None):
  409. """Parse an RFC 2822 atom.
  410. Optional atomends specifies a different set of end token delimiters
  411. (the default is to use self.atomends). This is used e.g. in
  412. getphraselist() since phrase endings must not include the `.' (which
  413. is legal in phrases)."""
  414. atomlist = ['']
  415. if atomends is None:
  416. atomends = self.atomends
  417. while self.pos < len(self.field):
  418. if self.field[self.pos] in atomends:
  419. break
  420. else:
  421. atomlist.append(self.field[self.pos])
  422. self.pos += 1
  423. return EMPTYSTRING.join(atomlist)
  424. def getphraselist(self):
  425. """Parse a sequence of RFC 2822 phrases.
  426. A phrase is a sequence of words, which are in turn either RFC 2822
  427. atoms or quoted-strings. Phrases are canonicalized by squeezing all
  428. runs of continuous whitespace into one space.
  429. """
  430. plist = []
  431. while self.pos < len(self.field):
  432. if self.field[self.pos] in self.FWS:
  433. self.pos += 1
  434. elif self.field[self.pos] == '"':
  435. plist.append(self.getquote())
  436. elif self.field[self.pos] == '(':
  437. self.commentlist.append(self.getcomment())
  438. elif self.field[self.pos] in self.phraseends:
  439. break
  440. else:
  441. plist.append(self.getatom(self.phraseends))
  442. return plist
  443. class AddressList(AddrlistClass):
  444. """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
  445. def __init__(self, field):
  446. AddrlistClass.__init__(self, field)
  447. if field:
  448. self.addresslist = self.getaddrlist()
  449. else:
  450. self.addresslist = []
  451. def __len__(self):
  452. return len(self.addresslist)
  453. def __add__(self, other):
  454. # Set union
  455. newaddr = AddressList(None)
  456. newaddr.addresslist = self.addresslist[:]
  457. for x in other.addresslist:
  458. if not x in self.addresslist:
  459. newaddr.addresslist.append(x)
  460. return newaddr
  461. def __iadd__(self, other):
  462. # Set union, in-place
  463. for x in other.addresslist:
  464. if not x in self.addresslist:
  465. self.addresslist.append(x)
  466. return self
  467. def __sub__(self, other):
  468. # Set difference
  469. newaddr = AddressList(None)
  470. for x in self.addresslist:
  471. if not x in other.addresslist:
  472. newaddr.addresslist.append(x)
  473. return newaddr
  474. def __isub__(self, other):
  475. # Set difference, in-place
  476. for x in other.addresslist:
  477. if x in self.addresslist:
  478. self.addresslist.remove(x)
  479. return self
  480. def __getitem__(self, index):
  481. # Make indexing, slices, and 'in' work
  482. return self.addresslist[index]