Maildir.py 8.7 KB

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