mailbox.py 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240
  1. """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
  2. # Notes for authors of new mailbox subclasses:
  3. #
  4. # Remember to fsync() changes to disk before closing a modified file
  5. # or returning from a flush() method. See functions _sync_flush() and
  6. # _sync_close().
  7. import sys
  8. import os
  9. import time
  10. import calendar
  11. import socket
  12. import errno
  13. import copy
  14. import email
  15. import email.message
  16. import email.generator
  17. import StringIO
  18. try:
  19. if sys.platform == 'os2emx':
  20. # OS/2 EMX fcntl() not adequate
  21. raise ImportError
  22. import fcntl
  23. except ImportError:
  24. fcntl = None
  25. import warnings
  26. with warnings.catch_warnings():
  27. if sys.py3kwarning:
  28. warnings.filterwarnings("ignore", ".*rfc822 has been removed",
  29. DeprecationWarning)
  30. import rfc822
  31. __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
  32. 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
  33. 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
  34. 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
  35. class Mailbox:
  36. """A group of messages in a particular place."""
  37. def __init__(self, path, factory=None, create=True):
  38. """Initialize a Mailbox instance."""
  39. self._path = os.path.abspath(os.path.expanduser(path))
  40. self._factory = factory
  41. def add(self, message):
  42. """Add message and return assigned key."""
  43. raise NotImplementedError('Method must be implemented by subclass')
  44. def remove(self, key):
  45. """Remove the keyed message; raise KeyError if it doesn't exist."""
  46. raise NotImplementedError('Method must be implemented by subclass')
  47. def __delitem__(self, key):
  48. self.remove(key)
  49. def discard(self, key):
  50. """If the keyed message exists, remove it."""
  51. try:
  52. self.remove(key)
  53. except KeyError:
  54. pass
  55. def __setitem__(self, key, message):
  56. """Replace the keyed message; raise KeyError if it doesn't exist."""
  57. raise NotImplementedError('Method must be implemented by subclass')
  58. def get(self, key, default=None):
  59. """Return the keyed message, or default if it doesn't exist."""
  60. try:
  61. return self.__getitem__(key)
  62. except KeyError:
  63. return default
  64. def __getitem__(self, key):
  65. """Return the keyed message; raise KeyError if it doesn't exist."""
  66. if not self._factory:
  67. return self.get_message(key)
  68. else:
  69. return self._factory(self.get_file(key))
  70. def get_message(self, key):
  71. """Return a Message representation or raise a KeyError."""
  72. raise NotImplementedError('Method must be implemented by subclass')
  73. def get_string(self, key):
  74. """Return a string representation or raise a KeyError."""
  75. raise NotImplementedError('Method must be implemented by subclass')
  76. def get_file(self, key):
  77. """Return a file-like representation or raise a KeyError."""
  78. raise NotImplementedError('Method must be implemented by subclass')
  79. def iterkeys(self):
  80. """Return an iterator over keys."""
  81. raise NotImplementedError('Method must be implemented by subclass')
  82. def keys(self):
  83. """Return a list of keys."""
  84. return list(self.iterkeys())
  85. def itervalues(self):
  86. """Return an iterator over all messages."""
  87. for key in self.iterkeys():
  88. try:
  89. value = self[key]
  90. except KeyError:
  91. continue
  92. yield value
  93. def __iter__(self):
  94. return self.itervalues()
  95. def values(self):
  96. """Return a list of messages. Memory intensive."""
  97. return list(self.itervalues())
  98. def iteritems(self):
  99. """Return an iterator over (key, message) tuples."""
  100. for key in self.iterkeys():
  101. try:
  102. value = self[key]
  103. except KeyError:
  104. continue
  105. yield (key, value)
  106. def items(self):
  107. """Return a list of (key, message) tuples. Memory intensive."""
  108. return list(self.iteritems())
  109. def has_key(self, key):
  110. """Return True if the keyed message exists, False otherwise."""
  111. raise NotImplementedError('Method must be implemented by subclass')
  112. def __contains__(self, key):
  113. return self.has_key(key)
  114. def __len__(self):
  115. """Return a count of messages in the mailbox."""
  116. raise NotImplementedError('Method must be implemented by subclass')
  117. def clear(self):
  118. """Delete all messages."""
  119. for key in self.iterkeys():
  120. self.discard(key)
  121. def pop(self, key, default=None):
  122. """Delete the keyed message and return it, or default."""
  123. try:
  124. result = self[key]
  125. except KeyError:
  126. return default
  127. self.discard(key)
  128. return result
  129. def popitem(self):
  130. """Delete an arbitrary (key, message) pair and return it."""
  131. for key in self.iterkeys():
  132. return (key, self.pop(key)) # This is only run once.
  133. else:
  134. raise KeyError('No messages in mailbox')
  135. def update(self, arg=None):
  136. """Change the messages that correspond to certain keys."""
  137. if hasattr(arg, 'iteritems'):
  138. source = arg.iteritems()
  139. elif hasattr(arg, 'items'):
  140. source = arg.items()
  141. else:
  142. source = arg
  143. bad_key = False
  144. for key, message in source:
  145. try:
  146. self[key] = message
  147. except KeyError:
  148. bad_key = True
  149. if bad_key:
  150. raise KeyError('No message with key(s)')
  151. def flush(self):
  152. """Write any pending changes to the disk."""
  153. raise NotImplementedError('Method must be implemented by subclass')
  154. def lock(self):
  155. """Lock the mailbox."""
  156. raise NotImplementedError('Method must be implemented by subclass')
  157. def unlock(self):
  158. """Unlock the mailbox if it is locked."""
  159. raise NotImplementedError('Method must be implemented by subclass')
  160. def close(self):
  161. """Flush and close the mailbox."""
  162. raise NotImplementedError('Method must be implemented by subclass')
  163. # Whether each message must end in a newline
  164. _append_newline = False
  165. def _dump_message(self, message, target, mangle_from_=False):
  166. # Most files are opened in binary mode to allow predictable seeking.
  167. # To get native line endings on disk, the user-friendly \n line endings
  168. # used in strings and by email.Message are translated here.
  169. """Dump message contents to target file."""
  170. if isinstance(message, email.message.Message):
  171. buffer = StringIO.StringIO()
  172. gen = email.generator.Generator(buffer, mangle_from_, 0)
  173. gen.flatten(message)
  174. buffer.seek(0)
  175. data = buffer.read().replace('\n', os.linesep)
  176. target.write(data)
  177. if self._append_newline and not data.endswith(os.linesep):
  178. # Make sure the message ends with a newline
  179. target.write(os.linesep)
  180. elif isinstance(message, str):
  181. if mangle_from_:
  182. message = message.replace('\nFrom ', '\n>From ')
  183. message = message.replace('\n', os.linesep)
  184. target.write(message)
  185. if self._append_newline and not message.endswith(os.linesep):
  186. # Make sure the message ends with a newline
  187. target.write(os.linesep)
  188. elif hasattr(message, 'read'):
  189. lastline = None
  190. while True:
  191. line = message.readline()
  192. if line == '':
  193. break
  194. if mangle_from_ and line.startswith('From '):
  195. line = '>From ' + line[5:]
  196. line = line.replace('\n', os.linesep)
  197. target.write(line)
  198. lastline = line
  199. if self._append_newline and lastline and not lastline.endswith(os.linesep):
  200. # Make sure the message ends with a newline
  201. target.write(os.linesep)
  202. else:
  203. raise TypeError('Invalid message type: %s' % type(message))
  204. class Maildir(Mailbox):
  205. """A qmail-style Maildir mailbox."""
  206. colon = ':'
  207. def __init__(self, dirname, factory=rfc822.Message, create=True):
  208. """Initialize a Maildir instance."""
  209. Mailbox.__init__(self, dirname, factory, create)
  210. self._paths = {
  211. 'tmp': os.path.join(self._path, 'tmp'),
  212. 'new': os.path.join(self._path, 'new'),
  213. 'cur': os.path.join(self._path, 'cur'),
  214. }
  215. if not os.path.exists(self._path):
  216. if create:
  217. os.mkdir(self._path, 0700)
  218. for path in self._paths.values():
  219. os.mkdir(path, 0o700)
  220. else:
  221. raise NoSuchMailboxError(self._path)
  222. self._toc = {}
  223. self._toc_mtimes = {'cur': 0, 'new': 0}
  224. self._last_read = 0 # Records last time we read cur/new
  225. self._skewfactor = 0.1 # Adjust if os/fs clocks are skewing
  226. def add(self, message):
  227. """Add message and return assigned key."""
  228. tmp_file = self._create_tmp()
  229. try:
  230. self._dump_message(message, tmp_file)
  231. except BaseException:
  232. tmp_file.close()
  233. os.remove(tmp_file.name)
  234. raise
  235. _sync_close(tmp_file)
  236. if isinstance(message, MaildirMessage):
  237. subdir = message.get_subdir()
  238. suffix = self.colon + message.get_info()
  239. if suffix == self.colon:
  240. suffix = ''
  241. else:
  242. subdir = 'new'
  243. suffix = ''
  244. uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
  245. dest = os.path.join(self._path, subdir, uniq + suffix)
  246. if isinstance(message, MaildirMessage):
  247. os.utime(tmp_file.name,
  248. (os.path.getatime(tmp_file.name), message.get_date()))
  249. # No file modification should be done after the file is moved to its
  250. # final position in order to prevent race conditions with changes
  251. # from other programs
  252. try:
  253. if hasattr(os, 'link'):
  254. os.link(tmp_file.name, dest)
  255. os.remove(tmp_file.name)
  256. else:
  257. os.rename(tmp_file.name, dest)
  258. except OSError, e:
  259. os.remove(tmp_file.name)
  260. if e.errno == errno.EEXIST:
  261. raise ExternalClashError('Name clash with existing message: %s'
  262. % dest)
  263. else:
  264. raise
  265. return uniq
  266. def remove(self, key):
  267. """Remove the keyed message; raise KeyError if it doesn't exist."""
  268. os.remove(os.path.join(self._path, self._lookup(key)))
  269. def discard(self, key):
  270. """If the keyed message exists, remove it."""
  271. # This overrides an inapplicable implementation in the superclass.
  272. try:
  273. self.remove(key)
  274. except KeyError:
  275. pass
  276. except OSError, e:
  277. if e.errno != errno.ENOENT:
  278. raise
  279. def __setitem__(self, key, message):
  280. """Replace the keyed message; raise KeyError if it doesn't exist."""
  281. old_subpath = self._lookup(key)
  282. temp_key = self.add(message)
  283. temp_subpath = self._lookup(temp_key)
  284. if isinstance(message, MaildirMessage):
  285. # temp's subdir and suffix were specified by message.
  286. dominant_subpath = temp_subpath
  287. else:
  288. # temp's subdir and suffix were defaults from add().
  289. dominant_subpath = old_subpath
  290. subdir = os.path.dirname(dominant_subpath)
  291. if self.colon in dominant_subpath:
  292. suffix = self.colon + dominant_subpath.split(self.colon)[-1]
  293. else:
  294. suffix = ''
  295. self.discard(key)
  296. tmp_path = os.path.join(self._path, temp_subpath)
  297. new_path = os.path.join(self._path, subdir, key + suffix)
  298. if isinstance(message, MaildirMessage):
  299. os.utime(tmp_path,
  300. (os.path.getatime(tmp_path), message.get_date()))
  301. # No file modification should be done after the file is moved to its
  302. # final position in order to prevent race conditions with changes
  303. # from other programs
  304. os.rename(tmp_path, new_path)
  305. def get_message(self, key):
  306. """Return a Message representation or raise a KeyError."""
  307. subpath = self._lookup(key)
  308. f = open(os.path.join(self._path, subpath), 'r')
  309. try:
  310. if self._factory:
  311. msg = self._factory(f)
  312. else:
  313. msg = MaildirMessage(f)
  314. finally:
  315. f.close()
  316. subdir, name = os.path.split(subpath)
  317. msg.set_subdir(subdir)
  318. if self.colon in name:
  319. msg.set_info(name.split(self.colon)[-1])
  320. msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
  321. return msg
  322. def get_string(self, key):
  323. """Return a string representation or raise a KeyError."""
  324. f = open(os.path.join(self._path, self._lookup(key)), 'r')
  325. try:
  326. return f.read()
  327. finally:
  328. f.close()
  329. def get_file(self, key):
  330. """Return a file-like representation or raise a KeyError."""
  331. f = open(os.path.join(self._path, self._lookup(key)), 'rb')
  332. return _ProxyFile(f)
  333. def iterkeys(self):
  334. """Return an iterator over keys."""
  335. self._refresh()
  336. for key in self._toc:
  337. try:
  338. self._lookup(key)
  339. except KeyError:
  340. continue
  341. yield key
  342. def has_key(self, key):
  343. """Return True if the keyed message exists, False otherwise."""
  344. self._refresh()
  345. return key in self._toc
  346. def __len__(self):
  347. """Return a count of messages in the mailbox."""
  348. self._refresh()
  349. return len(self._toc)
  350. def flush(self):
  351. """Write any pending changes to disk."""
  352. # Maildir changes are always written immediately, so there's nothing
  353. # to do.
  354. pass
  355. def lock(self):
  356. """Lock the mailbox."""
  357. return
  358. def unlock(self):
  359. """Unlock the mailbox if it is locked."""
  360. return
  361. def close(self):
  362. """Flush and close the mailbox."""
  363. return
  364. def list_folders(self):
  365. """Return a list of folder names."""
  366. result = []
  367. for entry in os.listdir(self._path):
  368. if len(entry) > 1 and entry[0] == '.' and \
  369. os.path.isdir(os.path.join(self._path, entry)):
  370. result.append(entry[1:])
  371. return result
  372. def get_folder(self, folder):
  373. """Return a Maildir instance for the named folder."""
  374. return Maildir(os.path.join(self._path, '.' + folder),
  375. factory=self._factory,
  376. create=False)
  377. def add_folder(self, folder):
  378. """Create a folder and return a Maildir instance representing it."""
  379. path = os.path.join(self._path, '.' + folder)
  380. result = Maildir(path, factory=self._factory)
  381. maildirfolder_path = os.path.join(path, 'maildirfolder')
  382. if not os.path.exists(maildirfolder_path):
  383. os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY,
  384. 0666))
  385. return result
  386. def remove_folder(self, folder):
  387. """Delete the named folder, which must be empty."""
  388. path = os.path.join(self._path, '.' + folder)
  389. for entry in os.listdir(os.path.join(path, 'new')) + \
  390. os.listdir(os.path.join(path, 'cur')):
  391. if len(entry) < 1 or entry[0] != '.':
  392. raise NotEmptyError('Folder contains message(s): %s' % folder)
  393. for entry in os.listdir(path):
  394. if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
  395. os.path.isdir(os.path.join(path, entry)):
  396. raise NotEmptyError("Folder contains subdirectory '%s': %s" %
  397. (folder, entry))
  398. for root, dirs, files in os.walk(path, topdown=False):
  399. for entry in files:
  400. os.remove(os.path.join(root, entry))
  401. for entry in dirs:
  402. os.rmdir(os.path.join(root, entry))
  403. os.rmdir(path)
  404. def clean(self):
  405. """Delete old files in "tmp"."""
  406. now = time.time()
  407. for entry in os.listdir(os.path.join(self._path, 'tmp')):
  408. path = os.path.join(self._path, 'tmp', entry)
  409. if now - os.path.getatime(path) > 129600: # 60 * 60 * 36
  410. os.remove(path)
  411. _count = 1 # This is used to generate unique file names.
  412. def _create_tmp(self):
  413. """Create a file in the tmp subdirectory and open and return it."""
  414. now = time.time()
  415. hostname = socket.gethostname()
  416. if '/' in hostname:
  417. hostname = hostname.replace('/', r'\057')
  418. if ':' in hostname:
  419. hostname = hostname.replace(':', r'\072')
  420. uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
  421. Maildir._count, hostname)
  422. path = os.path.join(self._path, 'tmp', uniq)
  423. try:
  424. os.stat(path)
  425. except OSError, e:
  426. if e.errno == errno.ENOENT:
  427. Maildir._count += 1
  428. try:
  429. return _create_carefully(path)
  430. except OSError, e:
  431. if e.errno != errno.EEXIST:
  432. raise
  433. else:
  434. raise
  435. # Fall through to here if stat succeeded or open raised EEXIST.
  436. raise ExternalClashError('Name clash prevented file creation: %s' %
  437. path)
  438. def _refresh(self):
  439. """Update table of contents mapping."""
  440. # If it has been less than two seconds since the last _refresh() call,
  441. # we have to unconditionally re-read the mailbox just in case it has
  442. # been modified, because os.path.mtime() has a 2 sec resolution in the
  443. # most common worst case (FAT) and a 1 sec resolution typically. This
  444. # results in a few unnecessary re-reads when _refresh() is called
  445. # multiple times in that interval, but once the clock ticks over, we
  446. # will only re-read as needed. Because the filesystem might be being
  447. # served by an independent system with its own clock, we record and
  448. # compare with the mtimes from the filesystem. Because the other
  449. # system's clock might be skewing relative to our clock, we add an
  450. # extra delta to our wait. The default is one tenth second, but is an
  451. # instance variable and so can be adjusted if dealing with a
  452. # particularly skewed or irregular system.
  453. if time.time() - self._last_read > 2 + self._skewfactor:
  454. refresh = False
  455. for subdir in self._toc_mtimes:
  456. mtime = os.path.getmtime(self._paths[subdir])
  457. if mtime > self._toc_mtimes[subdir]:
  458. refresh = True
  459. self._toc_mtimes[subdir] = mtime
  460. if not refresh:
  461. return
  462. # Refresh toc
  463. self._toc = {}
  464. for subdir in self._toc_mtimes:
  465. path = self._paths[subdir]
  466. for entry in os.listdir(path):
  467. p = os.path.join(path, entry)
  468. if os.path.isdir(p):
  469. continue
  470. uniq = entry.split(self.colon)[0]
  471. self._toc[uniq] = os.path.join(subdir, entry)
  472. self._last_read = time.time()
  473. def _lookup(self, key):
  474. """Use TOC to return subpath for given key, or raise a KeyError."""
  475. try:
  476. if os.path.exists(os.path.join(self._path, self._toc[key])):
  477. return self._toc[key]
  478. except KeyError:
  479. pass
  480. self._refresh()
  481. try:
  482. return self._toc[key]
  483. except KeyError:
  484. raise KeyError('No message with key: %s' % key)
  485. # This method is for backward compatibility only.
  486. def next(self):
  487. """Return the next message in a one-time iteration."""
  488. if not hasattr(self, '_onetime_keys'):
  489. self._onetime_keys = self.iterkeys()
  490. while True:
  491. try:
  492. return self[self._onetime_keys.next()]
  493. except StopIteration:
  494. return None
  495. except KeyError:
  496. continue
  497. class _singlefileMailbox(Mailbox):
  498. """A single-file mailbox."""
  499. def __init__(self, path, factory=None, create=True):
  500. """Initialize a single-file mailbox."""
  501. Mailbox.__init__(self, path, factory, create)
  502. try:
  503. f = open(self._path, 'rb+')
  504. except IOError, e:
  505. if e.errno == errno.ENOENT:
  506. if create:
  507. f = open(self._path, 'wb+')
  508. else:
  509. raise NoSuchMailboxError(self._path)
  510. elif e.errno in (errno.EACCES, errno.EROFS):
  511. f = open(self._path, 'rb')
  512. else:
  513. raise
  514. self._file = f
  515. self._toc = None
  516. self._next_key = 0
  517. self._pending = False # No changes require rewriting the file.
  518. self._pending_sync = False # No need to sync the file
  519. self._locked = False
  520. self._file_length = None # Used to record mailbox size
  521. def add(self, message):
  522. """Add message and return assigned key."""
  523. self._lookup()
  524. self._toc[self._next_key] = self._append_message(message)
  525. self._next_key += 1
  526. # _append_message appends the message to the mailbox file. We
  527. # don't need a full rewrite + rename, sync is enough.
  528. self._pending_sync = True
  529. return self._next_key - 1
  530. def remove(self, key):
  531. """Remove the keyed message; raise KeyError if it doesn't exist."""
  532. self._lookup(key)
  533. del self._toc[key]
  534. self._pending = True
  535. def __setitem__(self, key, message):
  536. """Replace the keyed message; raise KeyError if it doesn't exist."""
  537. self._lookup(key)
  538. self._toc[key] = self._append_message(message)
  539. self._pending = True
  540. def iterkeys(self):
  541. """Return an iterator over keys."""
  542. self._lookup()
  543. for key in self._toc.keys():
  544. yield key
  545. def has_key(self, key):
  546. """Return True if the keyed message exists, False otherwise."""
  547. self._lookup()
  548. return key in self._toc
  549. def __len__(self):
  550. """Return a count of messages in the mailbox."""
  551. self._lookup()
  552. return len(self._toc)
  553. def lock(self):
  554. """Lock the mailbox."""
  555. if not self._locked:
  556. _lock_file(self._file)
  557. self._locked = True
  558. def unlock(self):
  559. """Unlock the mailbox if it is locked."""
  560. if self._locked:
  561. _unlock_file(self._file)
  562. self._locked = False
  563. def flush(self):
  564. """Write any pending changes to disk."""
  565. if not self._pending:
  566. if self._pending_sync:
  567. # Messages have only been added, so syncing the file
  568. # is enough.
  569. _sync_flush(self._file)
  570. self._pending_sync = False
  571. return
  572. # In order to be writing anything out at all, self._toc must
  573. # already have been generated (and presumably has been modified
  574. # by adding or deleting an item).
  575. assert self._toc is not None
  576. # Check length of self._file; if it's changed, some other process
  577. # has modified the mailbox since we scanned it.
  578. self._file.seek(0, 2)
  579. cur_len = self._file.tell()
  580. if cur_len != self._file_length:
  581. raise ExternalClashError('Size of mailbox file changed '
  582. '(expected %i, found %i)' %
  583. (self._file_length, cur_len))
  584. new_file = _create_temporary(self._path)
  585. try:
  586. new_toc = {}
  587. self._pre_mailbox_hook(new_file)
  588. for key in sorted(self._toc.keys()):
  589. start, stop = self._toc[key]
  590. self._file.seek(start)
  591. self._pre_message_hook(new_file)
  592. new_start = new_file.tell()
  593. while True:
  594. buffer = self._file.read(min(4096,
  595. stop - self._file.tell()))
  596. if buffer == '':
  597. break
  598. new_file.write(buffer)
  599. new_toc[key] = (new_start, new_file.tell())
  600. self._post_message_hook(new_file)
  601. self._file_length = new_file.tell()
  602. except:
  603. new_file.close()
  604. os.remove(new_file.name)
  605. raise
  606. _sync_close(new_file)
  607. # self._file is about to get replaced, so no need to sync.
  608. self._file.close()
  609. # Make sure the new file's mode is the same as the old file's
  610. mode = os.stat(self._path).st_mode
  611. os.chmod(new_file.name, mode)
  612. try:
  613. os.rename(new_file.name, self._path)
  614. except OSError, e:
  615. if e.errno == errno.EEXIST or \
  616. (os.name == 'os2' and e.errno == errno.EACCES):
  617. os.remove(self._path)
  618. os.rename(new_file.name, self._path)
  619. else:
  620. raise
  621. self._file = open(self._path, 'rb+')
  622. self._toc = new_toc
  623. self._pending = False
  624. self._pending_sync = False
  625. if self._locked:
  626. _lock_file(self._file, dotlock=False)
  627. def _pre_mailbox_hook(self, f):
  628. """Called before writing the mailbox to file f."""
  629. return
  630. def _pre_message_hook(self, f):
  631. """Called before writing each message to file f."""
  632. return
  633. def _post_message_hook(self, f):
  634. """Called after writing each message to file f."""
  635. return
  636. def close(self):
  637. """Flush and close the mailbox."""
  638. try:
  639. self.flush()
  640. finally:
  641. try:
  642. if self._locked:
  643. self.unlock()
  644. finally:
  645. self._file.close() # Sync has been done by self.flush() above.
  646. def _lookup(self, key=None):
  647. """Return (start, stop) or raise KeyError."""
  648. if self._toc is None:
  649. self._generate_toc()
  650. if key is not None:
  651. try:
  652. return self._toc[key]
  653. except KeyError:
  654. raise KeyError('No message with key: %s' % key)
  655. def _append_message(self, message):
  656. """Append message to mailbox and return (start, stop) offsets."""
  657. self._file.seek(0, 2)
  658. before = self._file.tell()
  659. if len(self._toc) == 0 and not self._pending:
  660. # This is the first message, and the _pre_mailbox_hook
  661. # hasn't yet been called. If self._pending is True,
  662. # messages have been removed, so _pre_mailbox_hook must
  663. # have been called already.
  664. self._pre_mailbox_hook(self._file)
  665. try:
  666. self._pre_message_hook(self._file)
  667. offsets = self._install_message(message)
  668. self._post_message_hook(self._file)
  669. except BaseException:
  670. self._file.truncate(before)
  671. raise
  672. self._file.flush()
  673. self._file_length = self._file.tell() # Record current length of mailbox
  674. return offsets
  675. class _mboxMMDF(_singlefileMailbox):
  676. """An mbox or MMDF mailbox."""
  677. _mangle_from_ = True
  678. def get_message(self, key):
  679. """Return a Message representation or raise a KeyError."""
  680. start, stop = self._lookup(key)
  681. self._file.seek(start)
  682. from_line = self._file.readline().replace(os.linesep, '')
  683. string = self._file.read(stop - self._file.tell())
  684. msg = self._message_factory(string.replace(os.linesep, '\n'))
  685. msg.set_from(from_line[5:])
  686. return msg
  687. def get_string(self, key, from_=False):
  688. """Return a string representation or raise a KeyError."""
  689. start, stop = self._lookup(key)
  690. self._file.seek(start)
  691. if not from_:
  692. self._file.readline()
  693. string = self._file.read(stop - self._file.tell())
  694. return string.replace(os.linesep, '\n')
  695. def get_file(self, key, from_=False):
  696. """Return a file-like representation or raise a KeyError."""
  697. start, stop = self._lookup(key)
  698. self._file.seek(start)
  699. if not from_:
  700. self._file.readline()
  701. return _PartialFile(self._file, self._file.tell(), stop)
  702. def _install_message(self, message):
  703. """Format a message and blindly write to self._file."""
  704. from_line = None
  705. if isinstance(message, str) and message.startswith('From '):
  706. newline = message.find('\n')
  707. if newline != -1:
  708. from_line = message[:newline]
  709. message = message[newline + 1:]
  710. else:
  711. from_line = message
  712. message = ''
  713. elif isinstance(message, _mboxMMDFMessage):
  714. from_line = 'From ' + message.get_from()
  715. elif isinstance(message, email.message.Message):
  716. from_line = message.get_unixfrom() # May be None.
  717. if from_line is None:
  718. from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
  719. start = self._file.tell()
  720. self._file.write(from_line + os.linesep)
  721. self._dump_message(message, self._file, self._mangle_from_)
  722. stop = self._file.tell()
  723. return (start, stop)
  724. class mbox(_mboxMMDF):
  725. """A classic mbox mailbox."""
  726. _mangle_from_ = True
  727. # All messages must end in a newline character, and
  728. # _post_message_hooks outputs an empty line between messages.
  729. _append_newline = True
  730. def __init__(self, path, factory=None, create=True):
  731. """Initialize an mbox mailbox."""
  732. self._message_factory = mboxMessage
  733. _mboxMMDF.__init__(self, path, factory, create)
  734. def _post_message_hook(self, f):
  735. """Called after writing each message to file f."""
  736. f.write(os.linesep)
  737. def _generate_toc(self):
  738. """Generate key-to-(start, stop) table of contents."""
  739. starts, stops = [], []
  740. last_was_empty = False
  741. self._file.seek(0)
  742. while True:
  743. line_pos = self._file.tell()
  744. line = self._file.readline()
  745. if line.startswith('From '):
  746. if len(stops) < len(starts):
  747. if last_was_empty:
  748. stops.append(line_pos - len(os.linesep))
  749. else:
  750. # The last line before the "From " line wasn't
  751. # blank, but we consider it a start of a
  752. # message anyway.
  753. stops.append(line_pos)
  754. starts.append(line_pos)
  755. last_was_empty = False
  756. elif not line:
  757. if last_was_empty:
  758. stops.append(line_pos - len(os.linesep))
  759. else:
  760. stops.append(line_pos)
  761. break
  762. elif line == os.linesep:
  763. last_was_empty = True
  764. else:
  765. last_was_empty = False
  766. self._toc = dict(enumerate(zip(starts, stops)))
  767. self._next_key = len(self._toc)
  768. self._file_length = self._file.tell()
  769. class MMDF(_mboxMMDF):
  770. """An MMDF mailbox."""
  771. def __init__(self, path, factory=None, create=True):
  772. """Initialize an MMDF mailbox."""
  773. self._message_factory = MMDFMessage
  774. _mboxMMDF.__init__(self, path, factory, create)
  775. def _pre_message_hook(self, f):
  776. """Called before writing each message to file f."""
  777. f.write('\001\001\001\001' + os.linesep)
  778. def _post_message_hook(self, f):
  779. """Called after writing each message to file f."""
  780. f.write(os.linesep + '\001\001\001\001' + os.linesep)
  781. def _generate_toc(self):
  782. """Generate key-to-(start, stop) table of contents."""
  783. starts, stops = [], []
  784. self._file.seek(0)
  785. next_pos = 0
  786. while True:
  787. line_pos = next_pos
  788. line = self._file.readline()
  789. next_pos = self._file.tell()
  790. if line.startswith('\001\001\001\001' + os.linesep):
  791. starts.append(next_pos)
  792. while True:
  793. line_pos = next_pos
  794. line = self._file.readline()
  795. next_pos = self._file.tell()
  796. if line == '\001\001\001\001' + os.linesep:
  797. stops.append(line_pos - len(os.linesep))
  798. break
  799. elif line == '':
  800. stops.append(line_pos)
  801. break
  802. elif line == '':
  803. break
  804. self._toc = dict(enumerate(zip(starts, stops)))
  805. self._next_key = len(self._toc)
  806. self._file.seek(0, 2)
  807. self._file_length = self._file.tell()
  808. class MH(Mailbox):
  809. """An MH mailbox."""
  810. def __init__(self, path, factory=None, create=True):
  811. """Initialize an MH instance."""
  812. Mailbox.__init__(self, path, factory, create)
  813. if not os.path.exists(self._path):
  814. if create:
  815. os.mkdir(self._path, 0700)
  816. os.close(os.open(os.path.join(self._path, '.mh_sequences'),
  817. os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
  818. else:
  819. raise NoSuchMailboxError(self._path)
  820. self._locked = False
  821. def add(self, message):
  822. """Add message and return assigned key."""
  823. keys = self.keys()
  824. if len(keys) == 0:
  825. new_key = 1
  826. else:
  827. new_key = max(keys) + 1
  828. new_path = os.path.join(self._path, str(new_key))
  829. f = _create_carefully(new_path)
  830. closed = False
  831. try:
  832. if self._locked:
  833. _lock_file(f)
  834. try:
  835. try:
  836. self._dump_message(message, f)
  837. except BaseException:
  838. # Unlock and close so it can be deleted on Windows
  839. if self._locked:
  840. _unlock_file(f)
  841. _sync_close(f)
  842. closed = True
  843. os.remove(new_path)
  844. raise
  845. if isinstance(message, MHMessage):
  846. self._dump_sequences(message, new_key)
  847. finally:
  848. if self._locked:
  849. _unlock_file(f)
  850. finally:
  851. if not closed:
  852. _sync_close(f)
  853. return new_key
  854. def remove(self, key):
  855. """Remove the keyed message; raise KeyError if it doesn't exist."""
  856. path = os.path.join(self._path, str(key))
  857. try:
  858. f = open(path, 'rb+')
  859. except IOError, e:
  860. if e.errno == errno.ENOENT:
  861. raise KeyError('No message with key: %s' % key)
  862. else:
  863. raise
  864. else:
  865. f.close()
  866. os.remove(path)
  867. def __setitem__(self, key, message):
  868. """Replace the keyed message; raise KeyError if it doesn't exist."""
  869. path = os.path.join(self._path, str(key))
  870. try:
  871. f = open(path, 'rb+')
  872. except IOError, e:
  873. if e.errno == errno.ENOENT:
  874. raise KeyError('No message with key: %s' % key)
  875. else:
  876. raise
  877. try:
  878. if self._locked:
  879. _lock_file(f)
  880. try:
  881. os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
  882. self._dump_message(message, f)
  883. if isinstance(message, MHMessage):
  884. self._dump_sequences(message, key)
  885. finally:
  886. if self._locked:
  887. _unlock_file(f)
  888. finally:
  889. _sync_close(f)
  890. def get_message(self, key):
  891. """Return a Message representation or raise a KeyError."""
  892. try:
  893. if self._locked:
  894. f = open(os.path.join(self._path, str(key)), 'r+')
  895. else:
  896. f = open(os.path.join(self._path, str(key)), 'r')
  897. except IOError, e:
  898. if e.errno == errno.ENOENT:
  899. raise KeyError('No message with key: %s' % key)
  900. else:
  901. raise
  902. try:
  903. if self._locked:
  904. _lock_file(f)
  905. try:
  906. msg = MHMessage(f)
  907. finally:
  908. if self._locked:
  909. _unlock_file(f)
  910. finally:
  911. f.close()
  912. for name, key_list in self.get_sequences().iteritems():
  913. if key in key_list:
  914. msg.add_sequence(name)
  915. return msg
  916. def get_string(self, key):
  917. """Return a string representation or raise a KeyError."""
  918. try:
  919. if self._locked:
  920. f = open(os.path.join(self._path, str(key)), 'r+')
  921. else:
  922. f = open(os.path.join(self._path, str(key)), 'r')
  923. except IOError, e:
  924. if e.errno == errno.ENOENT:
  925. raise KeyError('No message with key: %s' % key)
  926. else:
  927. raise
  928. try:
  929. if self._locked:
  930. _lock_file(f)
  931. try:
  932. return f.read()
  933. finally:
  934. if self._locked:
  935. _unlock_file(f)
  936. finally:
  937. f.close()
  938. def get_file(self, key):
  939. """Return a file-like representation or raise a KeyError."""
  940. try:
  941. f = open(os.path.join(self._path, str(key)), 'rb')
  942. except IOError, e:
  943. if e.errno == errno.ENOENT:
  944. raise KeyError('No message with key: %s' % key)
  945. else:
  946. raise
  947. return _ProxyFile(f)
  948. def iterkeys(self):
  949. """Return an iterator over keys."""
  950. return iter(sorted(int(entry) for entry in os.listdir(self._path)
  951. if entry.isdigit()))
  952. def has_key(self, key):
  953. """Return True if the keyed message exists, False otherwise."""
  954. return os.path.exists(os.path.join(self._path, str(key)))
  955. def __len__(self):
  956. """Return a count of messages in the mailbox."""
  957. return len(list(self.iterkeys()))
  958. def lock(self):
  959. """Lock the mailbox."""
  960. if not self._locked:
  961. self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
  962. _lock_file(self._file)
  963. self._locked = True
  964. def unlock(self):
  965. """Unlock the mailbox if it is locked."""
  966. if self._locked:
  967. _unlock_file(self._file)
  968. _sync_close(self._file)
  969. del self._file
  970. self._locked = False
  971. def flush(self):
  972. """Write any pending changes to the disk."""
  973. return
  974. def close(self):
  975. """Flush and close the mailbox."""
  976. if self._locked:
  977. self.unlock()
  978. def list_folders(self):
  979. """Return a list of folder names."""
  980. result = []
  981. for entry in os.listdir(self._path):
  982. if os.path.isdir(os.path.join(self._path, entry)):
  983. result.append(entry)
  984. return result
  985. def get_folder(self, folder):
  986. """Return an MH instance for the named folder."""
  987. return MH(os.path.join(self._path, folder),
  988. factory=self._factory, create=False)
  989. def add_folder(self, folder):
  990. """Create a folder and return an MH instance representing it."""
  991. return MH(os.path.join(self._path, folder),
  992. factory=self._factory)
  993. def remove_folder(self, folder):
  994. """Delete the named folder, which must be empty."""
  995. path = os.path.join(self._path, folder)
  996. entries = os.listdir(path)
  997. if entries == ['.mh_sequences']:
  998. os.remove(os.path.join(path, '.mh_sequences'))
  999. elif entries == []:
  1000. pass
  1001. else:
  1002. raise NotEmptyError('Folder not empty: %s' % self._path)
  1003. os.rmdir(path)
  1004. def get_sequences(self):
  1005. """Return a name-to-key-list dictionary to define each sequence."""
  1006. results = {}
  1007. f = open(os.path.join(self._path, '.mh_sequences'), 'r')
  1008. try:
  1009. all_keys = set(self.keys())
  1010. for line in f:
  1011. try:
  1012. name, contents = line.split(':')
  1013. keys = set()
  1014. for spec in contents.split():
  1015. if spec.isdigit():
  1016. keys.add(int(spec))
  1017. else:
  1018. start, stop = (int(x) for x in spec.split('-'))
  1019. keys.update(range(start, stop + 1))
  1020. results[name] = [key for key in sorted(keys) \
  1021. if key in all_keys]
  1022. if len(results[name]) == 0:
  1023. del results[name]
  1024. except ValueError:
  1025. raise FormatError('Invalid sequence specification: %s' %
  1026. line.rstrip())
  1027. finally:
  1028. f.close()
  1029. return results
  1030. def set_sequences(self, sequences):
  1031. """Set sequences using the given name-to-key-list dictionary."""
  1032. f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
  1033. try:
  1034. os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
  1035. for name, keys in sequences.iteritems():
  1036. if len(keys) == 0:
  1037. continue
  1038. f.write('%s:' % name)
  1039. prev = None
  1040. completing = False
  1041. for key in sorted(set(keys)):
  1042. if key - 1 == prev:
  1043. if not completing:
  1044. completing = True
  1045. f.write('-')
  1046. elif completing:
  1047. completing = False
  1048. f.write('%s %s' % (prev, key))
  1049. else:
  1050. f.write(' %s' % key)
  1051. prev = key
  1052. if completing:
  1053. f.write(str(prev) + '\n')
  1054. else:
  1055. f.write('\n')
  1056. finally:
  1057. _sync_close(f)
  1058. def pack(self):
  1059. """Re-name messages to eliminate numbering gaps. Invalidates keys."""
  1060. sequences = self.get_sequences()
  1061. prev = 0
  1062. changes = []
  1063. for key in self.iterkeys():
  1064. if key - 1 != prev:
  1065. changes.append((key, prev + 1))
  1066. if hasattr(os, 'link'):
  1067. os.link(os.path.join(self._path, str(key)),
  1068. os.path.join(self._path, str(prev + 1)))
  1069. os.unlink(os.path.join(self._path, str(key)))
  1070. else:
  1071. os.rename(os.path.join(self._path, str(key)),
  1072. os.path.join(self._path, str(prev + 1)))
  1073. prev += 1
  1074. self._next_key = prev + 1
  1075. if len(changes) == 0:
  1076. return
  1077. for name, key_list in sequences.items():
  1078. for old, new in changes:
  1079. if old in key_list:
  1080. key_list[key_list.index(old)] = new
  1081. self.set_sequences(sequences)
  1082. def _dump_sequences(self, message, key):
  1083. """Inspect a new MHMessage and update sequences appropriately."""
  1084. pending_sequences = message.get_sequences()
  1085. all_sequences = self.get_sequences()
  1086. for name, key_list in all_sequences.iteritems():
  1087. if name in pending_sequences:
  1088. key_list.append(key)
  1089. elif key in key_list:
  1090. del key_list[key_list.index(key)]
  1091. for sequence in pending_sequences:
  1092. if sequence not in all_sequences:
  1093. all_sequences[sequence] = [key]
  1094. self.set_sequences(all_sequences)
  1095. class Babyl(_singlefileMailbox):
  1096. """An Rmail-style Babyl mailbox."""
  1097. _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
  1098. 'forwarded', 'edited', 'resent'))
  1099. def __init__(self, path, factory=None, create=True):
  1100. """Initialize a Babyl mailbox."""
  1101. _singlefileMailbox.__init__(self, path, factory, create)
  1102. self._labels = {}
  1103. def add(self, message):
  1104. """Add message and return assigned key."""
  1105. key = _singlefileMailbox.add(self, message)
  1106. if isinstance(message, BabylMessage):
  1107. self._labels[key] = message.get_labels()
  1108. return key
  1109. def remove(self, key):
  1110. """Remove the keyed message; raise KeyError if it doesn't exist."""
  1111. _singlefileMailbox.remove(self, key)
  1112. if key in self._labels:
  1113. del self._labels[key]
  1114. def __setitem__(self, key, message):
  1115. """Replace the keyed message; raise KeyError if it doesn't exist."""
  1116. _singlefileMailbox.__setitem__(self, key, message)
  1117. if isinstance(message, BabylMessage):
  1118. self._labels[key] = message.get_labels()
  1119. def get_message(self, key):
  1120. """Return a Message representation or raise a KeyError."""
  1121. start, stop = self._lookup(key)
  1122. self._file.seek(start)
  1123. self._file.readline() # Skip '1,' line specifying labels.
  1124. original_headers = StringIO.StringIO()
  1125. while True:
  1126. line = self._file.readline()
  1127. if line == '*** EOOH ***' + os.linesep or line == '':
  1128. break
  1129. original_headers.write(line.replace(os.linesep, '\n'))
  1130. visible_headers = StringIO.StringIO()
  1131. while True:
  1132. line = self._file.readline()
  1133. if line == os.linesep or line == '':
  1134. break
  1135. visible_headers.write(line.replace(os.linesep, '\n'))
  1136. body = self._file.read(stop - self._file.tell()).replace(os.linesep,
  1137. '\n')
  1138. msg = BabylMessage(original_headers.getvalue() + body)
  1139. msg.set_visible(visible_headers.getvalue())
  1140. if key in self._labels:
  1141. msg.set_labels(self._labels[key])
  1142. return msg
  1143. def get_string(self, key):
  1144. """Return a string representation or raise a KeyError."""
  1145. start, stop = self._lookup(key)
  1146. self._file.seek(start)
  1147. self._file.readline() # Skip '1,' line specifying labels.
  1148. original_headers = StringIO.StringIO()
  1149. while True:
  1150. line = self._file.readline()
  1151. if line == '*** EOOH ***' + os.linesep or line == '':
  1152. break
  1153. original_headers.write(line.replace(os.linesep, '\n'))
  1154. while True:
  1155. line = self._file.readline()
  1156. if line == os.linesep or line == '':
  1157. break
  1158. return original_headers.getvalue() + \
  1159. self._file.read(stop - self._file.tell()).replace(os.linesep,
  1160. '\n')
  1161. def get_file(self, key):
  1162. """Return a file-like representation or raise a KeyError."""
  1163. return StringIO.StringIO(self.get_string(key).replace('\n',
  1164. os.linesep))
  1165. def get_labels(self):
  1166. """Return a list of user-defined labels in the mailbox."""
  1167. self._lookup()
  1168. labels = set()
  1169. for label_list in self._labels.values():
  1170. labels.update(label_list)
  1171. labels.difference_update(self._special_labels)
  1172. return list(labels)
  1173. def _generate_toc(self):
  1174. """Generate key-to-(start, stop) table of contents."""
  1175. starts, stops = [], []
  1176. self._file.seek(0)
  1177. next_pos = 0
  1178. label_lists = []
  1179. while True:
  1180. line_pos = next_pos
  1181. line = self._file.readline()
  1182. next_pos = self._file.tell()
  1183. if line == '\037\014' + os.linesep:
  1184. if len(stops) < len(starts):
  1185. stops.append(line_pos - len(os.linesep))
  1186. starts.append(next_pos)
  1187. labels = [label.strip() for label
  1188. in self._file.readline()[1:].split(',')
  1189. if label.strip() != '']
  1190. label_lists.append(labels)
  1191. elif line == '\037' or line == '\037' + os.linesep:
  1192. if len(stops) < len(starts):
  1193. stops.append(line_pos - len(os.linesep))
  1194. elif line == '':
  1195. stops.append(line_pos - len(os.linesep))
  1196. break
  1197. self._toc = dict(enumerate(zip(starts, stops)))
  1198. self._labels = dict(enumerate(label_lists))
  1199. self._next_key = len(self._toc)
  1200. self._file.seek(0, 2)
  1201. self._file_length = self._file.tell()
  1202. def _pre_mailbox_hook(self, f):
  1203. """Called before writing the mailbox to file f."""
  1204. f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
  1205. (os.linesep, os.linesep, ','.join(self.get_labels()),
  1206. os.linesep))
  1207. def _pre_message_hook(self, f):
  1208. """Called before writing each message to file f."""
  1209. f.write('\014' + os.linesep)
  1210. def _post_message_hook(self, f):
  1211. """Called after writing each message to file f."""
  1212. f.write(os.linesep + '\037')
  1213. def _install_message(self, message):
  1214. """Write message contents and return (start, stop)."""
  1215. start = self._file.tell()
  1216. if isinstance(message, BabylMessage):
  1217. special_labels = []
  1218. labels = []
  1219. for label in message.get_labels():
  1220. if label in self._special_labels:
  1221. special_labels.append(label)
  1222. else:
  1223. labels.append(label)
  1224. self._file.write('1')
  1225. for label in special_labels:
  1226. self._file.write(', ' + label)
  1227. self._file.write(',,')
  1228. for label in labels:
  1229. self._file.write(' ' + label + ',')
  1230. self._file.write(os.linesep)
  1231. else:
  1232. self._file.write('1,,' + os.linesep)
  1233. if isinstance(message, email.message.Message):
  1234. orig_buffer = StringIO.StringIO()
  1235. orig_generator = email.generator.Generator(orig_buffer, False, 0)
  1236. orig_generator.flatten(message)
  1237. orig_buffer.seek(0)
  1238. while True:
  1239. line = orig_buffer.readline()
  1240. self._file.write(line.replace('\n', os.linesep))
  1241. if line == '\n' or line == '':
  1242. break
  1243. self._file.write('*** EOOH ***' + os.linesep)
  1244. if isinstance(message, BabylMessage):
  1245. vis_buffer = StringIO.StringIO()
  1246. vis_generator = email.generator.Generator(vis_buffer, False, 0)
  1247. vis_generator.flatten(message.get_visible())
  1248. while True:
  1249. line = vis_buffer.readline()
  1250. self._file.write(line.replace('\n', os.linesep))
  1251. if line == '\n' or line == '':
  1252. break
  1253. else:
  1254. orig_buffer.seek(0)
  1255. while True:
  1256. line = orig_buffer.readline()
  1257. self._file.write(line.replace('\n', os.linesep))
  1258. if line == '\n' or line == '':
  1259. break
  1260. while True:
  1261. buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
  1262. if buffer == '':
  1263. break
  1264. self._file.write(buffer.replace('\n', os.linesep))
  1265. elif isinstance(message, str):
  1266. body_start = message.find('\n\n') + 2
  1267. if body_start - 2 != -1:
  1268. self._file.write(message[:body_start].replace('\n',
  1269. os.linesep))
  1270. self._file.write('*** EOOH ***' + os.linesep)
  1271. self._file.write(message[:body_start].replace('\n',
  1272. os.linesep))
  1273. self._file.write(message[body_start:].replace('\n',
  1274. os.linesep))
  1275. else:
  1276. self._file.write('*** EOOH ***' + os.linesep + os.linesep)
  1277. self._file.write(message.replace('\n', os.linesep))
  1278. elif hasattr(message, 'readline'):
  1279. original_pos = message.tell()
  1280. first_pass = True
  1281. while True:
  1282. line = message.readline()
  1283. self._file.write(line.replace('\n', os.linesep))
  1284. if line == '\n' or line == '':
  1285. if first_pass:
  1286. first_pass = False
  1287. self._file.write('*** EOOH ***' + os.linesep)
  1288. message.seek(original_pos)
  1289. else:
  1290. break
  1291. while True:
  1292. buffer = message.read(4096) # Buffer size is arbitrary.
  1293. if buffer == '':
  1294. break
  1295. self._file.write(buffer.replace('\n', os.linesep))
  1296. else:
  1297. raise TypeError('Invalid message type: %s' % type(message))
  1298. stop = self._file.tell()
  1299. return (start, stop)
  1300. class Message(email.message.Message):
  1301. """Message with mailbox-format-specific properties."""
  1302. def __init__(self, message=None):
  1303. """Initialize a Message instance."""
  1304. if isinstance(message, email.message.Message):
  1305. self._become_message(copy.deepcopy(message))
  1306. if isinstance(message, Message):
  1307. message._explain_to(self)
  1308. elif isinstance(message, str):
  1309. self._become_message(email.message_from_string(message))
  1310. elif hasattr(message, "read"):
  1311. self._become_message(email.message_from_file(message))
  1312. elif message is None:
  1313. email.message.Message.__init__(self)
  1314. else:
  1315. raise TypeError('Invalid message type: %s' % type(message))
  1316. def _become_message(self, message):
  1317. """Assume the non-format-specific state of message."""
  1318. for name in ('_headers', '_unixfrom', '_payload', '_charset',
  1319. 'preamble', 'epilogue', 'defects', '_default_type'):
  1320. self.__dict__[name] = message.__dict__[name]
  1321. def _explain_to(self, message):
  1322. """Copy format-specific state to message insofar as possible."""
  1323. if isinstance(message, Message):
  1324. return # There's nothing format-specific to explain.
  1325. else:
  1326. raise TypeError('Cannot convert to specified type')
  1327. class MaildirMessage(Message):
  1328. """Message with Maildir-specific properties."""
  1329. def __init__(self, message=None):
  1330. """Initialize a MaildirMessage instance."""
  1331. self._subdir = 'new'
  1332. self._info = ''
  1333. self._date = time.time()
  1334. Message.__init__(self, message)
  1335. def get_subdir(self):
  1336. """Return 'new' or 'cur'."""
  1337. return self._subdir
  1338. def set_subdir(self, subdir):
  1339. """Set subdir to 'new' or 'cur'."""
  1340. if subdir == 'new' or subdir == 'cur':
  1341. self._subdir = subdir
  1342. else:
  1343. raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
  1344. def get_flags(self):
  1345. """Return as a string the flags that are set."""
  1346. if self._info.startswith('2,'):
  1347. return self._info[2:]
  1348. else:
  1349. return ''
  1350. def set_flags(self, flags):
  1351. """Set the given flags and unset all others."""
  1352. self._info = '2,' + ''.join(sorted(flags))
  1353. def add_flag(self, flag):
  1354. """Set the given flag(s) without changing others."""
  1355. self.set_flags(''.join(set(self.get_flags()) | set(flag)))
  1356. def remove_flag(self, flag):
  1357. """Unset the given string flag(s) without changing others."""
  1358. if self.get_flags() != '':
  1359. self.set_flags(''.join(set(self.get_flags()) - set(flag)))
  1360. def get_date(self):
  1361. """Return delivery date of message, in seconds since the epoch."""
  1362. return self._date
  1363. def set_date(self, date):
  1364. """Set delivery date of message, in seconds since the epoch."""
  1365. try:
  1366. self._date = float(date)
  1367. except ValueError:
  1368. raise TypeError("can't convert to float: %s" % date)
  1369. def get_info(self):
  1370. """Get the message's "info" as a string."""
  1371. return self._info
  1372. def set_info(self, info):
  1373. """Set the message's "info" string."""
  1374. if isinstance(info, str):
  1375. self._info = info
  1376. else:
  1377. raise TypeError('info must be a string: %s' % type(info))
  1378. def _explain_to(self, message):
  1379. """Copy Maildir-specific state to message insofar as possible."""
  1380. if isinstance(message, MaildirMessage):
  1381. message.set_flags(self.get_flags())
  1382. message.set_subdir(self.get_subdir())
  1383. message.set_date(self.get_date())
  1384. elif isinstance(message, _mboxMMDFMessage):
  1385. flags = set(self.get_flags())
  1386. if 'S' in flags:
  1387. message.add_flag('R')
  1388. if self.get_subdir() == 'cur':
  1389. message.add_flag('O')
  1390. if 'T' in flags:
  1391. message.add_flag('D')
  1392. if 'F' in flags:
  1393. message.add_flag('F')
  1394. if 'R' in flags:
  1395. message.add_flag('A')
  1396. message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
  1397. elif isinstance(message, MHMessage):
  1398. flags = set(self.get_flags())
  1399. if 'S' not in flags:
  1400. message.add_sequence('unseen')
  1401. if 'R' in flags:
  1402. message.add_sequence('replied')
  1403. if 'F' in flags:
  1404. message.add_sequence('flagged')
  1405. elif isinstance(message, BabylMessage):
  1406. flags = set(self.get_flags())
  1407. if 'S' not in flags:
  1408. message.add_label('unseen')
  1409. if 'T' in flags:
  1410. message.add_label('deleted')
  1411. if 'R' in flags:
  1412. message.add_label('answered')
  1413. if 'P' in flags:
  1414. message.add_label('forwarded')
  1415. elif isinstance(message, Message):
  1416. pass
  1417. else:
  1418. raise TypeError('Cannot convert to specified type: %s' %
  1419. type(message))
  1420. class _mboxMMDFMessage(Message):
  1421. """Message with mbox- or MMDF-specific properties."""
  1422. def __init__(self, message=None):
  1423. """Initialize an mboxMMDFMessage instance."""
  1424. self.set_from('MAILER-DAEMON', True)
  1425. if isinstance(message, email.message.Message):
  1426. unixfrom = message.get_unixfrom()
  1427. if unixfrom is not None and unixfrom.startswith('From '):
  1428. self.set_from(unixfrom[5:])
  1429. Message.__init__(self, message)
  1430. def get_from(self):
  1431. """Return contents of "From " line."""
  1432. return self._from
  1433. def set_from(self, from_, time_=None):
  1434. """Set "From " line, formatting and appending time_ if specified."""
  1435. if time_ is not None:
  1436. if time_ is True:
  1437. time_ = time.gmtime()
  1438. from_ += ' ' + time.asctime(time_)
  1439. self._from = from_
  1440. def get_flags(self):
  1441. """Return as a string the flags that are set."""
  1442. return self.get('Status', '') + self.get('X-Status', '')
  1443. def set_flags(self, flags):
  1444. """Set the given flags and unset all others."""
  1445. flags = set(flags)
  1446. status_flags, xstatus_flags = '', ''
  1447. for flag in ('R', 'O'):
  1448. if flag in flags:
  1449. status_flags += flag
  1450. flags.remove(flag)
  1451. for flag in ('D', 'F', 'A'):
  1452. if flag in flags:
  1453. xstatus_flags += flag
  1454. flags.remove(flag)
  1455. xstatus_flags += ''.join(sorted(flags))
  1456. try:
  1457. self.replace_header('Status', status_flags)
  1458. except KeyError:
  1459. self.add_header('Status', status_flags)
  1460. try:
  1461. self.replace_header('X-Status', xstatus_flags)
  1462. except KeyError:
  1463. self.add_header('X-Status', xstatus_flags)
  1464. def add_flag(self, flag):
  1465. """Set the given flag(s) without changing others."""
  1466. self.set_flags(''.join(set(self.get_flags()) | set(flag)))
  1467. def remove_flag(self, flag):
  1468. """Unset the given string flag(s) without changing others."""
  1469. if 'Status' in self or 'X-Status' in self:
  1470. self.set_flags(''.join(set(self.get_flags()) - set(flag)))
  1471. def _explain_to(self, message):
  1472. """Copy mbox- or MMDF-specific state to message insofar as possible."""
  1473. if isinstance(message, MaildirMessage):
  1474. flags = set(self.get_flags())
  1475. if 'O' in flags:
  1476. message.set_subdir('cur')
  1477. if 'F' in flags:
  1478. message.add_flag('F')
  1479. if 'A' in flags:
  1480. message.add_flag('R')
  1481. if 'R' in flags:
  1482. message.add_flag('S')
  1483. if 'D' in flags:
  1484. message.add_flag('T')
  1485. del message['status']
  1486. del message['x-status']
  1487. maybe_date = ' '.join(self.get_from().split()[-5:])
  1488. try:
  1489. message.set_date(calendar.timegm(time.strptime(maybe_date,
  1490. '%a %b %d %H:%M:%S %Y')))
  1491. except (ValueError, OverflowError):
  1492. pass
  1493. elif isinstance(message, _mboxMMDFMessage):
  1494. message.set_flags(self.get_flags())
  1495. message.set_from(self.get_from())
  1496. elif isinstance(message, MHMessage):
  1497. flags = set(self.get_flags())
  1498. if 'R' not in flags:
  1499. message.add_sequence('unseen')
  1500. if 'A' in flags:
  1501. message.add_sequence('replied')
  1502. if 'F' in flags:
  1503. message.add_sequence('flagged')
  1504. del message['status']
  1505. del message['x-status']
  1506. elif isinstance(message, BabylMessage):
  1507. flags = set(self.get_flags())
  1508. if 'R' not in flags:
  1509. message.add_label('unseen')
  1510. if 'D' in flags:
  1511. message.add_label('deleted')
  1512. if 'A' in flags:
  1513. message.add_label('answered')
  1514. del message['status']
  1515. del message['x-status']
  1516. elif isinstance(message, Message):
  1517. pass
  1518. else:
  1519. raise TypeError('Cannot convert to specified type: %s' %
  1520. type(message))
  1521. class mboxMessage(_mboxMMDFMessage):
  1522. """Message with mbox-specific properties."""
  1523. class MHMessage(Message):
  1524. """Message with MH-specific properties."""
  1525. def __init__(self, message=None):
  1526. """Initialize an MHMessage instance."""
  1527. self._sequences = []
  1528. Message.__init__(self, message)
  1529. def get_sequences(self):
  1530. """Return a list of sequences that include the message."""
  1531. return self._sequences[:]
  1532. def set_sequences(self, sequences):
  1533. """Set the list of sequences that include the message."""
  1534. self._sequences = list(sequences)
  1535. def add_sequence(self, sequence):
  1536. """Add sequence to list of sequences including the message."""
  1537. if isinstance(sequence, str):
  1538. if not sequence in self._sequences:
  1539. self._sequences.append(sequence)
  1540. else:
  1541. raise TypeError('sequence must be a string: %s' % type(sequence))
  1542. def remove_sequence(self, sequence):
  1543. """Remove sequence from the list of sequences including the message."""
  1544. try:
  1545. self._sequences.remove(sequence)
  1546. except ValueError:
  1547. pass
  1548. def _explain_to(self, message):
  1549. """Copy MH-specific state to message insofar as possible."""
  1550. if isinstance(message, MaildirMessage):
  1551. sequences = set(self.get_sequences())
  1552. if 'unseen' in sequences:
  1553. message.set_subdir('cur')
  1554. else:
  1555. message.set_subdir('cur')
  1556. message.add_flag('S')
  1557. if 'flagged' in sequences:
  1558. message.add_flag('F')
  1559. if 'replied' in sequences:
  1560. message.add_flag('R')
  1561. elif isinstance(message, _mboxMMDFMessage):
  1562. sequences = set(self.get_sequences())
  1563. if 'unseen' not in sequences:
  1564. message.add_flag('RO')
  1565. else:
  1566. message.add_flag('O')
  1567. if 'flagged' in sequences:
  1568. message.add_flag('F')
  1569. if 'replied' in sequences:
  1570. message.add_flag('A')
  1571. elif isinstance(message, MHMessage):
  1572. for sequence in self.get_sequences():
  1573. message.add_sequence(sequence)
  1574. elif isinstance(message, BabylMessage):
  1575. sequences = set(self.get_sequences())
  1576. if 'unseen' in sequences:
  1577. message.add_label('unseen')
  1578. if 'replied' in sequences:
  1579. message.add_label('answered')
  1580. elif isinstance(message, Message):
  1581. pass
  1582. else:
  1583. raise TypeError('Cannot convert to specified type: %s' %
  1584. type(message))
  1585. class BabylMessage(Message):
  1586. """Message with Babyl-specific properties."""
  1587. def __init__(self, message=None):
  1588. """Initialize a BabylMessage instance."""
  1589. self._labels = []
  1590. self._visible = Message()
  1591. Message.__init__(self, message)
  1592. def get_labels(self):
  1593. """Return a list of labels on the message."""
  1594. return self._labels[:]
  1595. def set_labels(self, labels):
  1596. """Set the list of labels on the message."""
  1597. self._labels = list(labels)
  1598. def add_label(self, label):
  1599. """Add label to list of labels on the message."""
  1600. if isinstance(label, str):
  1601. if label not in self._labels:
  1602. self._labels.append(label)
  1603. else:
  1604. raise TypeError('label must be a string: %s' % type(label))
  1605. def remove_label(self, label):
  1606. """Remove label from the list of labels on the message."""
  1607. try:
  1608. self._labels.remove(label)
  1609. except ValueError:
  1610. pass
  1611. def get_visible(self):
  1612. """Return a Message representation of visible headers."""
  1613. return Message(self._visible)
  1614. def set_visible(self, visible):
  1615. """Set the Message representation of visible headers."""
  1616. self._visible = Message(visible)
  1617. def update_visible(self):
  1618. """Update and/or sensibly generate a set of visible headers."""
  1619. for header in self._visible.keys():
  1620. if header in self:
  1621. self._visible.replace_header(header, self[header])
  1622. else:
  1623. del self._visible[header]
  1624. for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
  1625. if header in self and header not in self._visible:
  1626. self._visible[header] = self[header]
  1627. def _explain_to(self, message):
  1628. """Copy Babyl-specific state to message insofar as possible."""
  1629. if isinstance(message, MaildirMessage):
  1630. labels = set(self.get_labels())
  1631. if 'unseen' in labels:
  1632. message.set_subdir('cur')
  1633. else:
  1634. message.set_subdir('cur')
  1635. message.add_flag('S')
  1636. if 'forwarded' in labels or 'resent' in labels:
  1637. message.add_flag('P')
  1638. if 'answered' in labels:
  1639. message.add_flag('R')
  1640. if 'deleted' in labels:
  1641. message.add_flag('T')
  1642. elif isinstance(message, _mboxMMDFMessage):
  1643. labels = set(self.get_labels())
  1644. if 'unseen' not in labels:
  1645. message.add_flag('RO')
  1646. else:
  1647. message.add_flag('O')
  1648. if 'deleted' in labels:
  1649. message.add_flag('D')
  1650. if 'answered' in labels:
  1651. message.add_flag('A')
  1652. elif isinstance(message, MHMessage):
  1653. labels = set(self.get_labels())
  1654. if 'unseen' in labels:
  1655. message.add_sequence('unseen')
  1656. if 'answered' in labels:
  1657. message.add_sequence('replied')
  1658. elif isinstance(message, BabylMessage):
  1659. message.set_visible(self.get_visible())
  1660. for label in self.get_labels():
  1661. message.add_label(label)
  1662. elif isinstance(message, Message):
  1663. pass
  1664. else:
  1665. raise TypeError('Cannot convert to specified type: %s' %
  1666. type(message))
  1667. class MMDFMessage(_mboxMMDFMessage):
  1668. """Message with MMDF-specific properties."""
  1669. class _ProxyFile:
  1670. """A read-only wrapper of a file."""
  1671. def __init__(self, f, pos=None):
  1672. """Initialize a _ProxyFile."""
  1673. self._file = f
  1674. if pos is None:
  1675. self._pos = f.tell()
  1676. else:
  1677. self._pos = pos
  1678. def read(self, size=None):
  1679. """Read bytes."""
  1680. return self._read(size, self._file.read)
  1681. def readline(self, size=None):
  1682. """Read a line."""
  1683. return self._read(size, self._file.readline)
  1684. def readlines(self, sizehint=None):
  1685. """Read multiple lines."""
  1686. result = []
  1687. for line in self:
  1688. result.append(line)
  1689. if sizehint is not None:
  1690. sizehint -= len(line)
  1691. if sizehint <= 0:
  1692. break
  1693. return result
  1694. def __iter__(self):
  1695. """Iterate over lines."""
  1696. return iter(self.readline, "")
  1697. def tell(self):
  1698. """Return the position."""
  1699. return self._pos
  1700. def seek(self, offset, whence=0):
  1701. """Change position."""
  1702. if whence == 1:
  1703. self._file.seek(self._pos)
  1704. self._file.seek(offset, whence)
  1705. self._pos = self._file.tell()
  1706. def close(self):
  1707. """Close the file."""
  1708. if hasattr(self, '_file'):
  1709. if hasattr(self._file, 'close'):
  1710. self._file.close()
  1711. del self._file
  1712. def _read(self, size, read_method):
  1713. """Read size bytes using read_method."""
  1714. if size is None:
  1715. size = -1
  1716. self._file.seek(self._pos)
  1717. result = read_method(size)
  1718. self._pos = self._file.tell()
  1719. return result
  1720. class _PartialFile(_ProxyFile):
  1721. """A read-only wrapper of part of a file."""
  1722. def __init__(self, f, start=None, stop=None):
  1723. """Initialize a _PartialFile."""
  1724. _ProxyFile.__init__(self, f, start)
  1725. self._start = start
  1726. self._stop = stop
  1727. def tell(self):
  1728. """Return the position with respect to start."""
  1729. return _ProxyFile.tell(self) - self._start
  1730. def seek(self, offset, whence=0):
  1731. """Change position, possibly with respect to start or stop."""
  1732. if whence == 0:
  1733. self._pos = self._start
  1734. whence = 1
  1735. elif whence == 2:
  1736. self._pos = self._stop
  1737. whence = 1
  1738. _ProxyFile.seek(self, offset, whence)
  1739. def _read(self, size, read_method):
  1740. """Read size bytes using read_method, honoring start and stop."""
  1741. remaining = self._stop - self._pos
  1742. if remaining <= 0:
  1743. return ''
  1744. if size is None or size < 0 or size > remaining:
  1745. size = remaining
  1746. return _ProxyFile._read(self, size, read_method)
  1747. def close(self):
  1748. # do *not* close the underlying file object for partial files,
  1749. # since it's global to the mailbox object
  1750. if hasattr(self, '_file'):
  1751. del self._file
  1752. def _lock_file(f, dotlock=True):
  1753. """Lock file f using lockf and dot locking."""
  1754. dotlock_done = False
  1755. try:
  1756. if fcntl:
  1757. try:
  1758. fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
  1759. except IOError, e:
  1760. if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS):
  1761. raise ExternalClashError('lockf: lock unavailable: %s' %
  1762. f.name)
  1763. else:
  1764. raise
  1765. if dotlock:
  1766. try:
  1767. pre_lock = _create_temporary(f.name + '.lock')
  1768. pre_lock.close()
  1769. except IOError, e:
  1770. if e.errno in (errno.EACCES, errno.EROFS):
  1771. return # Without write access, just skip dotlocking.
  1772. else:
  1773. raise
  1774. try:
  1775. if hasattr(os, 'link'):
  1776. os.link(pre_lock.name, f.name + '.lock')
  1777. dotlock_done = True
  1778. os.unlink(pre_lock.name)
  1779. else:
  1780. os.rename(pre_lock.name, f.name + '.lock')
  1781. dotlock_done = True
  1782. except OSError, e:
  1783. if e.errno == errno.EEXIST or \
  1784. (os.name == 'os2' and e.errno == errno.EACCES):
  1785. os.remove(pre_lock.name)
  1786. raise ExternalClashError('dot lock unavailable: %s' %
  1787. f.name)
  1788. else:
  1789. raise
  1790. except:
  1791. if fcntl:
  1792. fcntl.lockf(f, fcntl.LOCK_UN)
  1793. if dotlock_done:
  1794. os.remove(f.name + '.lock')
  1795. raise
  1796. def _unlock_file(f):
  1797. """Unlock file f using lockf and dot locking."""
  1798. if fcntl:
  1799. fcntl.lockf(f, fcntl.LOCK_UN)
  1800. if os.path.exists(f.name + '.lock'):
  1801. os.remove(f.name + '.lock')
  1802. def _create_carefully(path):
  1803. """Create a file if it doesn't exist and open for reading and writing."""
  1804. fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0666)
  1805. try:
  1806. return open(path, 'rb+')
  1807. finally:
  1808. os.close(fd)
  1809. def _create_temporary(path):
  1810. """Create a temp file based on path and open for reading and writing."""
  1811. return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
  1812. socket.gethostname(),
  1813. os.getpid()))
  1814. def _sync_flush(f):
  1815. """Ensure changes to file f are physically on disk."""
  1816. f.flush()
  1817. if hasattr(os, 'fsync'):
  1818. os.fsync(f.fileno())
  1819. def _sync_close(f):
  1820. """Close file f, ensuring all changes are physically on disk."""
  1821. _sync_flush(f)
  1822. f.close()
  1823. ## Start: classes from the original module (for backward compatibility).
  1824. # Note that the Maildir class, whose name is unchanged, itself offers a next()
  1825. # method for backward compatibility.
  1826. class _Mailbox:
  1827. def __init__(self, fp, factory=rfc822.Message):
  1828. self.fp = fp
  1829. self.seekp = 0
  1830. self.factory = factory
  1831. def __iter__(self):
  1832. return iter(self.next, None)
  1833. def next(self):
  1834. while 1:
  1835. self.fp.seek(self.seekp)
  1836. try:
  1837. self._search_start()
  1838. except EOFError:
  1839. self.seekp = self.fp.tell()
  1840. return None
  1841. start = self.fp.tell()
  1842. self._search_end()
  1843. self.seekp = stop = self.fp.tell()
  1844. if start != stop:
  1845. break
  1846. return self.factory(_PartialFile(self.fp, start, stop))
  1847. # Recommended to use PortableUnixMailbox instead!
  1848. class UnixMailbox(_Mailbox):
  1849. def _search_start(self):
  1850. while 1:
  1851. pos = self.fp.tell()
  1852. line = self.fp.readline()
  1853. if not line:
  1854. raise EOFError
  1855. if line[:5] == 'From ' and self._isrealfromline(line):
  1856. self.fp.seek(pos)
  1857. return
  1858. def _search_end(self):
  1859. self.fp.readline() # Throw away header line
  1860. while 1:
  1861. pos = self.fp.tell()
  1862. line = self.fp.readline()
  1863. if not line:
  1864. return
  1865. if line[:5] == 'From ' and self._isrealfromline(line):
  1866. self.fp.seek(pos)
  1867. return
  1868. # An overridable mechanism to test for From-line-ness. You can either
  1869. # specify a different regular expression or define a whole new
  1870. # _isrealfromline() method. Note that this only gets called for lines
  1871. # starting with the 5 characters "From ".
  1872. #
  1873. # BAW: According to
  1874. #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
  1875. # the only portable, reliable way to find message delimiters in a BSD (i.e
  1876. # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
  1877. # beginning of the file, "^From .*\n". While _fromlinepattern below seems
  1878. # like a good idea, in practice, there are too many variations for more
  1879. # strict parsing of the line to be completely accurate.
  1880. #
  1881. # _strict_isrealfromline() is the old version which tries to do stricter
  1882. # parsing of the From_ line. _portable_isrealfromline() simply returns
  1883. # true, since it's never called if the line doesn't already start with
  1884. # "From ".
  1885. #
  1886. # This algorithm, and the way it interacts with _search_start() and
  1887. # _search_end() may not be completely correct, because it doesn't check
  1888. # that the two characters preceding "From " are \n\n or the beginning of
  1889. # the file. Fixing this would require a more extensive rewrite than is
  1890. # necessary. For convenience, we've added a PortableUnixMailbox class
  1891. # which does no checking of the format of the 'From' line.
  1892. _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
  1893. r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
  1894. r"[^\s]*\s*"
  1895. "$")
  1896. _regexp = None
  1897. def _strict_isrealfromline(self, line):
  1898. if not self._regexp:
  1899. import re
  1900. self._regexp = re.compile(self._fromlinepattern)
  1901. return self._regexp.match(line)
  1902. def _portable_isrealfromline(self, line):
  1903. return True
  1904. _isrealfromline = _strict_isrealfromline
  1905. class PortableUnixMailbox(UnixMailbox):
  1906. _isrealfromline = UnixMailbox._portable_isrealfromline
  1907. class MmdfMailbox(_Mailbox):
  1908. def _search_start(self):
  1909. while 1:
  1910. line = self.fp.readline()
  1911. if not line:
  1912. raise EOFError
  1913. if line[:5] == '\001\001\001\001\n':
  1914. return
  1915. def _search_end(self):
  1916. while 1:
  1917. pos = self.fp.tell()
  1918. line = self.fp.readline()
  1919. if not line:
  1920. return
  1921. if line == '\001\001\001\001\n':
  1922. self.fp.seek(pos)
  1923. return
  1924. class MHMailbox:
  1925. def __init__(self, dirname, factory=rfc822.Message):
  1926. import re
  1927. pat = re.compile('^[1-9][0-9]*$')
  1928. self.dirname = dirname
  1929. # the three following lines could be combined into:
  1930. # list = map(long, filter(pat.match, os.listdir(self.dirname)))
  1931. list = os.listdir(self.dirname)
  1932. list = filter(pat.match, list)
  1933. list = map(long, list)
  1934. list.sort()
  1935. # This only works in Python 1.6 or later;
  1936. # before that str() added 'L':
  1937. self.boxes = map(str, list)
  1938. self.boxes.reverse()
  1939. self.factory = factory
  1940. def __iter__(self):
  1941. return iter(self.next, None)
  1942. def next(self):
  1943. if not self.boxes:
  1944. return None
  1945. fn = self.boxes.pop()
  1946. fp = open(os.path.join(self.dirname, fn))
  1947. msg = self.factory(fp)
  1948. try:
  1949. msg._mh_msgno = fn
  1950. except (AttributeError, TypeError):
  1951. pass
  1952. return msg
  1953. class BabylMailbox(_Mailbox):
  1954. def _search_start(self):
  1955. while 1:
  1956. line = self.fp.readline()
  1957. if not line:
  1958. raise EOFError
  1959. if line == '*** EOOH ***\n':
  1960. return
  1961. def _search_end(self):
  1962. while 1:
  1963. pos = self.fp.tell()
  1964. line = self.fp.readline()
  1965. if not line:
  1966. return
  1967. if line == '\037\014\n' or line == '\037':
  1968. self.fp.seek(pos)
  1969. return
  1970. ## End: classes from the original module (for backward compatibility).
  1971. class Error(Exception):
  1972. """Raised for module-specific errors."""
  1973. class NoSuchMailboxError(Error):
  1974. """The specified mailbox does not exist and won't be created."""
  1975. class NotEmptyError(Error):
  1976. """The specified mailbox is not empty and deletion was requested."""
  1977. class ExternalClashError(Error):
  1978. """Another process caused an action to fail."""
  1979. class FormatError(Error):
  1980. """A file appears to have an invalid format."""