Base.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. """ Base repository support
  2. Copyright (C) 2002-2017 John Goerzen & contributors
  3. This program is free software; you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation; either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program; if not, write to the Free Software
  13. Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  14. """
  15. import re
  16. import os.path
  17. from sys import exc_info
  18. from offlineimap import CustomConfig
  19. from offlineimap.ui import getglobalui
  20. from offlineimap.error import OfflineImapError
  21. class BaseRepository(CustomConfig.ConfigHelperMixin):
  22. """
  23. Base Class for Repository
  24. """
  25. def __init__(self, reposname, account):
  26. self.ui = getglobalui()
  27. self.account = account
  28. self.config = account.getconfig()
  29. self.name = reposname
  30. self.localeval = account.getlocaleval()
  31. self._accountname = self.account.getname()
  32. self._readonly = self.getconfboolean('readonly', False)
  33. self.uiddir = os.path.join(self.config.getmetadatadir(),
  34. 'Repository-' + self.name)
  35. if not os.path.exists(self.uiddir):
  36. os.mkdir(self.uiddir, 0o700)
  37. self.mapdir = os.path.join(self.uiddir, 'UIDMapping')
  38. if not os.path.exists(self.mapdir):
  39. os.mkdir(self.mapdir, 0o700)
  40. # FIXME: self.uiddir variable name is lying about itself.
  41. self.uiddir = os.path.join(self.uiddir, 'FolderValidity')
  42. if not os.path.exists(self.uiddir):
  43. os.mkdir(self.uiddir, 0o700)
  44. self.nametrans = lambda foldername: foldername
  45. self.folderfilter = lambda foldername: 1
  46. self.folderincludes = []
  47. self.foldersort = None
  48. self.newmail_hook = None
  49. if self.config.has_option(self.getsection(), 'nametrans'):
  50. self.nametrans = self.localeval.eval(
  51. self.getconf('nametrans'), {'re': re})
  52. if self.config.has_option(self.getsection(), 'folderfilter'):
  53. self.folderfilter = self.localeval.eval(
  54. self.getconf('folderfilter'), {'re': re})
  55. if self.config.has_option(self.getsection(), 'folderincludes'):
  56. self.folderincludes = self.localeval.eval(
  57. self.getconf('folderincludes'), {'re': re})
  58. if self.config.has_option(self.getsection(), 'foldersort'):
  59. self.foldersort = self.localeval.eval(
  60. self.getconf('foldersort'), {'re': re})
  61. def restore_atime(self):
  62. """Sets folders' atime back to their values after a sync
  63. Controlled by the 'restoreatime' config parameter (default
  64. False), applies only to local Maildir mailboxes and does nothing
  65. on all other repository types."""
  66. def connect(self):
  67. """Establish a connection to the remote, if necessary. This exists
  68. so that IMAP connections can all be established up front, gathering
  69. passwords as needed. It was added in order to support the
  70. error recovery -- we need to connect first outside of the error
  71. trap in order to validate the password, and that's the point of
  72. this function."""
  73. def holdordropconnections(self):
  74. """
  75. Hold the drop connections functions.
  76. Returns: None
  77. """
  78. def dropconnections(self):
  79. """
  80. Drop connections functions.
  81. Returns: None
  82. """
  83. def getaccount(self):
  84. """
  85. This patch returns the account
  86. Returns: The account
  87. """
  88. return self.account
  89. def getname(self):
  90. """
  91. Get the repository name
  92. Returns: String with the repository name
  93. """
  94. return self.name
  95. def __str__(self):
  96. return self.name
  97. @property
  98. def accountname(self):
  99. """Account name as string"""
  100. return self._accountname
  101. def getuiddir(self):
  102. """
  103. The FolderValidity directory
  104. Returns: The FolderValidity directory
  105. """
  106. return self.uiddir
  107. def getmapdir(self):
  108. """
  109. Get the map dir (UIDMapping)
  110. Returns: The UIDMapping directory
  111. """
  112. return self.mapdir
  113. # Interface from CustomConfig.ConfigHelperMixin
  114. def getsection(self):
  115. return 'Repository ' + self.name
  116. # Interface from CustomConfig.ConfigHelperMixin
  117. def getconfig(self):
  118. return self.config
  119. @property
  120. def readonly(self):
  121. """Is the repository readonly?"""
  122. return self._readonly
  123. def getlocaleval(self):
  124. """
  125. Get the account local eval.
  126. Returns: LocalEval class for account.
  127. """
  128. return self.account.getlocaleval()
  129. def getfolders(self):
  130. """Returns a list of ALL folders on this server."""
  131. return []
  132. def forgetfolders(self):
  133. """Forgets the cached list of folders, if any. Useful to run
  134. after a sync run."""
  135. def getsep(self):
  136. """
  137. Get the separator.
  138. This function is not implemented.
  139. Returns: None
  140. """
  141. raise NotImplementedError
  142. def getkeywordmap(self):
  143. """
  144. Get the keyword map.
  145. This function is not implemented.
  146. Returns: None
  147. """
  148. raise NotImplementedError
  149. def should_sync_folder(self, fname):
  150. """Should this folder be synced?"""
  151. return fname in self.folderincludes or self.folderfilter(fname)
  152. def should_create_folders(self):
  153. """Is folder creation enabled on this repository?
  154. It is disabled by either setting the whole repository
  155. 'readonly' or by using the 'createfolders' setting."""
  156. return (not self._readonly) and self.getconfboolean('createfolders',
  157. True)
  158. def makefolder(self, foldername):
  159. """
  160. Create a new folder.
  161. This function is not implemented
  162. Args:
  163. foldername: Folder to create
  164. Returns: None
  165. """
  166. raise NotImplementedError
  167. def deletefolder(self, foldername):
  168. """
  169. Remove the selected folder.
  170. This function is not implemented
  171. Args:
  172. foldername: Folder to delete
  173. Returns: None
  174. """
  175. raise NotImplementedError
  176. def getfolder(self, foldername, decode=True):
  177. """Get the folder for this repo.
  178. WARNING: the signature changes whether it's remote or local:
  179. - remote types have the decode arg
  180. - local types don't have the decode arg
  181. """
  182. raise NotImplementedError
  183. def sync_folder_structure(self, local_repo, status_repo):
  184. """Sync the folders structure.
  185. It does NOT sync the contents of those folders. nametrans rules
  186. in both directions will be honored
  187. Configuring nametrans on BOTH repositories could lead to infinite folder
  188. creation cycles."""
  189. if not self.should_create_folders()\
  190. and not local_repo.should_create_folders():
  191. # Quick exit if no folder creation is enabled on either side.
  192. return None
  193. remote_repo = self
  194. remote_hash, local_hash = {}, {}
  195. for folder in remote_repo.getfolders():
  196. remote_hash[folder.getname()] = folder
  197. for folder in local_repo.getfolders():
  198. local_hash[folder.getname()] = folder
  199. # Create new folders from remote to local.
  200. for remote_name, remote_folder in list(remote_hash.items()):
  201. # Don't create on local_repo, if it is readonly.
  202. if not local_repo.should_create_folders():
  203. break
  204. # Apply remote nametrans and fix serparator.
  205. local_name = remote_folder.getvisiblename().replace(
  206. remote_repo.getsep(), local_repo.getsep())
  207. if remote_folder.sync_this \
  208. and local_name not in list(local_hash.keys()):
  209. try:
  210. local_repo.makefolder(local_name)
  211. # Need to refresh list.
  212. local_repo.forgetfolders()
  213. except OfflineImapError as exc:
  214. self.ui.error(exc, exc_info()[2],
  215. "Creating folder %s on repository %s" %
  216. (local_name, local_repo))
  217. raise
  218. status_repo.makefolder(local_name.replace(
  219. local_repo.getsep(), status_repo.getsep()))
  220. # Create new folders from local to remote.
  221. for local_name, local_folder in list(local_hash.items()):
  222. if not remote_repo.should_create_folders():
  223. # Don't create missing folder on readonly repo.
  224. break
  225. # Apply reverse nametrans and fix serparator.
  226. remote_name = local_folder.getvisiblename().replace(
  227. local_repo.getsep(), remote_repo.getsep())
  228. if local_folder.sync_this \
  229. and remote_name not in list(remote_hash.keys()):
  230. # Would the remote filter out the new folder name? In this case
  231. # don't create it.
  232. if not remote_repo.should_sync_folder(remote_name):
  233. msg = "Not creating folder '%s' (repository '%s') " \
  234. "as it would be filtered out on that repository." % \
  235. (remote_name, self)
  236. self.ui.debug('', msg)
  237. continue
  238. # nametrans sanity check! Does remote nametrans lead to the
  239. # original local name?
  240. #
  241. # Apply remote nametrans to see if we end up with the same
  242. # name. We have:
  243. # - remote_name: local_name -> reverse nametrans + separator
  244. # We want local_name == loop_name from:
  245. # - remote_name -> remote (nametrans + separator) -> loop_name
  246. #
  247. # Get IMAPFolder and see if the reverse nametrans works fine.
  248. # TODO: getfolder() works only because we succeed in getting
  249. # inexisting folders which I would like to change. Take care!
  250. tmp_remotefolder = remote_repo.getfolder(remote_name,
  251. decode=False)
  252. loop_name = tmp_remotefolder.getvisiblename().replace(
  253. remote_repo.getsep(), local_repo.getsep())
  254. if local_name != loop_name:
  255. msg = "INFINITE FOLDER CREATION DETECTED! "\
  256. "Folder '%s' (repository '%s') would be created as " \
  257. "folder '%s' (repository '%s'). The latter " \
  258. "becomes '%s' in return, leading to infinite " \
  259. "folder creation cycles.\n "\
  260. "SOLUTION: 1) Do set your nametrans rules on both " \
  261. "repositories so they lead to identical names if " \
  262. "applied back and forth. " \
  263. "2) Use folderfilter settings on a repository to " \
  264. "prevent some folders from being created on the " \
  265. "other side." % \
  266. (local_folder.getname(), local_repo, remote_name,
  267. remote_repo, loop_name)
  268. raise OfflineImapError(msg, OfflineImapError.ERROR.REPO)
  269. # End sanity check, actually create the folder.
  270. try:
  271. remote_repo.makefolder(remote_name)
  272. # Need to refresh list.
  273. self.forgetfolders()
  274. except OfflineImapError as exc:
  275. msg = "Creating folder %s on repository %s" % \
  276. (remote_name, remote_repo)
  277. self.ui.error(exc, exc_info()[2], msg)
  278. raise
  279. status_repo.makefolder(local_name.replace(
  280. local_repo.getsep(), status_repo.getsep()))
  281. # Find deleted folders.
  282. # TODO: We don't delete folders right now.
  283. return None
  284. def startkeepalive(self):
  285. """The default implementation will do nothing."""
  286. def stopkeepalive(self):
  287. """Stop keep alive, but don't bother waiting
  288. for the threads to terminate."""
  289. def getlocalroot(self):
  290. """ Local root folder for storing messages.
  291. Will not be set for remote repositories."""
  292. return None