Maildir.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. """
  2. Maildir repository support
  3. Copyright (C) 2002-2015 John Goerzen & contributors
  4. This program is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation; either version 2 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program; if not, write to the Free Software
  14. Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  15. """
  16. import os
  17. from offlineimap import folder
  18. from offlineimap.ui import getglobalui
  19. from offlineimap.error import OfflineImapError
  20. from offlineimap.repository.Base import BaseRepository
  21. class MaildirRepository(BaseRepository):
  22. """
  23. Maildir Repository Class
  24. """
  25. def __init__(self, reposname, account):
  26. """Initialize a MaildirRepository object. Takes a path name
  27. to the directory holding all the Maildir directories."""
  28. BaseRepository.__init__(self, reposname, account)
  29. self.root = self.getlocalroot()
  30. self.folders = None
  31. self.ui = getglobalui()
  32. self.debug("MaildirRepository initialized, sep is %s" %
  33. repr(self.getsep()))
  34. self.folder_atimes = []
  35. # Create the top-level folder if it doesn't exist
  36. if not os.path.isdir(self.root):
  37. os.makedirs(self.root, 0o700)
  38. # Create the keyword->char mapping
  39. self.keyword2char = dict()
  40. for char in 'abcdefghijklmnopqrstuvwxyz':
  41. confkey = 'customflag_' + char
  42. keyword = self.getconf(confkey, None)
  43. if keyword is not None:
  44. self.keyword2char[keyword] = char
  45. def _append_folder_atimes(self, foldername):
  46. """Store the atimes of a folder's new|cur in self.folder_atimes"""
  47. path = os.path.join(self.root, foldername)
  48. new = os.path.join(path, 'new')
  49. cur = os.path.join(path, 'cur')
  50. atimes = (path, os.path.getatime(new), os.path.getatime(cur))
  51. self.folder_atimes.append(atimes)
  52. def restore_atime(self):
  53. """Sets folders' atime back to their values after a sync
  54. Controlled by the 'restoreatime' config parameter."""
  55. if not self.getconfboolean('restoreatime', False):
  56. return # not configured to restore
  57. for (dirpath, new_atime, cur_atime) in self.folder_atimes:
  58. new_dir = os.path.join(dirpath, 'new')
  59. cur_dir = os.path.join(dirpath, 'cur')
  60. os.utime(new_dir, (new_atime, os.path.getmtime(new_dir)))
  61. os.utime(cur_dir, (cur_atime, os.path.getmtime(cur_dir)))
  62. def getlocalroot(self):
  63. xforms = [os.path.expanduser, os.path.expandvars]
  64. return self.getconf_xform('localfolders', xforms)
  65. def debug(self, msg):
  66. """
  67. Debug function for the message. It calls the ui.debug function and
  68. prepends the string 'maildir'.
  69. Args:
  70. msg: Message to send to the debug
  71. Returns: None
  72. """
  73. self.ui.debug('maildir', msg)
  74. def getsep(self):
  75. return self.getconf('sep', '.').strip()
  76. def getkeywordmap(self):
  77. return self.keyword2char if len(self.keyword2char) > 0 else None
  78. def makefolder(self, foldername):
  79. """Create new Maildir folder if necessary
  80. This will not update the list cached in getfolders(). You will
  81. need to invoke :meth:`forgetfolders` to force new caching when
  82. you are done creating folders yourself.
  83. :param foldername: A relative mailbox name. The maildir will be
  84. created in self.root+'/'+foldername. All intermediate folder
  85. levels will be created if they do not exist yet. 'cur',
  86. 'tmp', and 'new' subfolders will be created in the maildir.
  87. """
  88. self.ui.makefolder(self, foldername)
  89. if self.account.dryrun:
  90. return
  91. full_path = os.path.abspath(os.path.join(self.root, foldername))
  92. # sanity tests
  93. if self.getsep() == '/':
  94. for component in foldername.split('/'):
  95. assert component not in ['new', 'cur', 'tmp'], \
  96. "When using nested folders (/ as a Maildir separator), " \
  97. "folder names may not contain 'new', 'cur', 'tmp'."
  98. assert foldername.find('../') == -1, \
  99. "Folder names may not contain ../"
  100. assert not foldername.startswith('/'), \
  101. "Folder names may not begin with /"
  102. # If we're using hierarchical folders, it's possible that
  103. # sub-folders may be created before higher-up ones.
  104. self.debug("makefolder: calling makedirs '%s'" % full_path)
  105. try:
  106. os.makedirs(full_path, 0o700)
  107. except OSError as exc:
  108. if exc.errno == 17 and os.path.isdir(full_path):
  109. self.debug("makefolder: '%s' already a directory" % foldername)
  110. else:
  111. raise
  112. for subdir in ['cur', 'new', 'tmp']:
  113. try:
  114. os.mkdir(os.path.join(full_path, subdir), 0o700)
  115. except OSError as exc:
  116. if exc.errno == 17 and os.path.isdir(full_path):
  117. self.debug("makefolder: '%s' already has subdir %s" %
  118. (foldername, subdir))
  119. else:
  120. raise
  121. def deletefolder(self, foldername):
  122. self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s" % foldername)
  123. def getfolder(self, foldername):
  124. """Return a Folder instance of this Maildir
  125. If necessary, scan and cache all foldernames to make sure that
  126. we only return existing folders and that 2 calls with the same
  127. name will return the same object."""
  128. # getfolders() will scan and cache the values *if* necessary
  129. folders = self.getfolders()
  130. for foldr in folders:
  131. if foldername == foldr.name:
  132. return foldr
  133. raise OfflineImapError("getfolder() asked for a nonexisting "
  134. "folder '%s'." % foldername,
  135. OfflineImapError.ERROR.FOLDER)
  136. def _getfolders_scandir(self, root, extension=None):
  137. """Recursively scan folder 'root'; return a list of MailDirFolder
  138. :param root: (absolute) path to Maildir root
  139. :param extension: (relative) subfolder to examine within root"""
  140. self.debug("_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s" %
  141. (root, extension))
  142. retval = []
  143. # Configure the full path to this repository -- "toppath"
  144. if extension:
  145. toppath = os.path.join(root, extension)
  146. else:
  147. toppath = root
  148. self.debug(" toppath = %s" % toppath)
  149. # Iterate over directories in top & top itself.
  150. for dirname in os.listdir(toppath) + ['']:
  151. self.debug(" dirname = %s" % dirname)
  152. if dirname == '' and extension is not None:
  153. self.debug(' skip this entry (already scanned)')
  154. continue
  155. if dirname in ['cur', 'new', 'tmp']:
  156. self.debug(" skip this entry (Maildir special)")
  157. # Bypass special files.
  158. continue
  159. fullname = os.path.join(toppath, dirname)
  160. if not os.path.isdir(fullname):
  161. self.debug(" skip this entry (not a directory)")
  162. # Not a directory -- not a folder.
  163. continue
  164. # extension can be None.
  165. if extension:
  166. foldername = os.path.join(extension, dirname)
  167. else:
  168. foldername = dirname
  169. if (os.path.isdir(os.path.join(fullname, 'cur')) and
  170. os.path.isdir(os.path.join(fullname, 'new')) and
  171. os.path.isdir(os.path.join(fullname, 'tmp'))):
  172. # This directory has maildir stuff -- process
  173. self.debug(" This is maildir folder '%s'." % foldername)
  174. if self.getconfboolean('restoreatime', False):
  175. self._append_folder_atimes(foldername)
  176. file_desc = self.getfoldertype()(self.root, foldername,
  177. self.getsep(), self)
  178. retval.append(file_desc)
  179. if self.getsep() == '/' and dirname != '':
  180. # Recursively check sub-directories for folders too.
  181. retval.extend(self._getfolders_scandir(root, foldername))
  182. self.debug("_GETFOLDERS_SCANDIR RETURNING %s" %
  183. repr([x.getname() for x in retval]))
  184. return retval
  185. def getfolders(self):
  186. if self.folders is None:
  187. self.folders = self._getfolders_scandir(self.root)
  188. return self.folders
  189. def getfoldertype(self):
  190. """
  191. Returns a folder MaildirFolder type.
  192. Returns: A MaildirFolder class
  193. """
  194. return folder.Maildir.MaildirFolder
  195. def forgetfolders(self):
  196. """Forgets the cached list of folders, if any. Useful to run
  197. after a sync run."""
  198. self.folders = None