IMAP.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. """
  2. IMAP repository support
  3. Copyright (C) 2002-2019 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. import netrc
  18. import errno
  19. from sys import exc_info
  20. from threading import Event
  21. from offlineimap import folder, imaputil, imapserver, OfflineImapError
  22. from offlineimap.repository.Base import BaseRepository
  23. from offlineimap.threadutil import ExitNotifyThread
  24. from offlineimap.utils.distro_utils import get_os_sslcertfile, \
  25. get_os_sslcertfile_searchpath
  26. class IMAPRepository(BaseRepository):
  27. """
  28. IMAP Repository Class, children of BaseRepository
  29. """
  30. def __init__(self, reposname, account):
  31. self.idlefolders = None
  32. BaseRepository.__init__(self, reposname, account)
  33. # self.ui is being set by the BaseRepository
  34. self._host = None
  35. # Must be set before calling imapserver.IMAPServer(self)
  36. self.oauth2_request_url = None
  37. self.imapserver = imapserver.IMAPServer(self)
  38. self.folders = None
  39. self.copy_ignore_eval = None
  40. # Keep alive.
  41. self.kaevent = None
  42. self.kathread = None
  43. # Only set the newmail_hook in an IMAP repository.
  44. if self.config.has_option(self.getsection(), 'newmail_hook'):
  45. self.newmail_hook = self.localeval.eval(
  46. self.getconf('newmail_hook'))
  47. if self.getconf('sep', None):
  48. self.ui.info("The 'sep' setting is being ignored for IMAP "
  49. "repository '%s' (it's autodetected)" % self)
  50. def startkeepalive(self):
  51. keepalivetime = self.getkeepalive()
  52. if not keepalivetime:
  53. return
  54. self.kaevent = Event()
  55. self.kathread = ExitNotifyThread(target=self.imapserver.keepalive,
  56. name="Keep alive " + self.getname(),
  57. args=(keepalivetime, self.kaevent))
  58. self.kathread.setDaemon(True)
  59. self.kathread.start()
  60. def stopkeepalive(self):
  61. if self.kaevent is None:
  62. return # Keepalive is not active.
  63. self.kaevent.set()
  64. self.kathread = None
  65. self.kaevent = None
  66. def holdordropconnections(self):
  67. if not self.getholdconnectionopen():
  68. self.dropconnections()
  69. def dropconnections(self):
  70. self.imapserver.close()
  71. def get_copy_ignore_UIDs(self, foldername):
  72. """Return a list of UIDs to not copy for this foldername."""
  73. if self.copy_ignore_eval is None:
  74. if self.config.has_option(self.getsection(),
  75. 'copy_ignore_eval'):
  76. self.copy_ignore_eval = self.localeval.eval(
  77. self.getconf('copy_ignore_eval'))
  78. else:
  79. self.copy_ignore_eval = lambda x: None
  80. return self.copy_ignore_eval(foldername)
  81. def getholdconnectionopen(self):
  82. """
  83. Value of holdconnectionopen or False if it is not set
  84. Returns: Value of holdconnectionopen or False if it is not set
  85. """
  86. if self.getidlefolders():
  87. return True
  88. return self.getconfboolean("holdconnectionopen", False)
  89. def getkeepalive(self):
  90. """
  91. This function returns the keepalive value. If it is not set, then
  92. check if the getidlefolders is set. If getidlefolders is set, then
  93. returns 29 * 60
  94. Returns: keepalive value
  95. """
  96. num = self.getconfint("keepalive", 0)
  97. if num == 0 and self.getidlefolders():
  98. return 29 * 60
  99. return num
  100. def getsep(self):
  101. """Return the folder separator for the IMAP repository
  102. This requires that self.imapserver has been initialized with an
  103. acquireconnection() or it will still be `None`"""
  104. assert self.imapserver.delim is not None, \
  105. "'%s' repository called getsep() before the folder separator was " \
  106. "queried from the server" % self
  107. return self.imapserver.delim
  108. def gethost(self):
  109. """Return the configured hostname to connect to
  110. :returns: hostname as string or throws Exception"""
  111. if self._host: # Use cached value if possible.
  112. return self._host
  113. # 1) Check for remotehosteval setting.
  114. if self.config.has_option(self.getsection(), 'remotehosteval'):
  115. host = self.getconf('remotehosteval')
  116. try:
  117. l_host = self.localeval.eval(host)
  118. # We need a str host
  119. if isinstance(l_host, bytes):
  120. return l_host.decode(encoding='utf-8')
  121. elif isinstance(l_host, str):
  122. return l_host
  123. # If is not bytes or str, we have a problem
  124. raise OfflineImapError("Could not get a right host format for"
  125. " repository %s. Type found: %s. "
  126. "Please, open a bug." %
  127. (self.name, type(l_host)),
  128. OfflineImapError.ERROR.FOLDER)
  129. except Exception as exc:
  130. raise OfflineImapError(
  131. "remotehosteval option for repository "
  132. "'%s' failed:\n%s" % (self, exc),
  133. OfflineImapError.ERROR.REPO,
  134. exc_info()[2]) from exc
  135. if host:
  136. self._host = host
  137. return self._host
  138. # 2) Check for plain remotehost setting.
  139. host = self.getconf('remotehost', None)
  140. if host is not None:
  141. self._host = host
  142. return self._host
  143. # No success.
  144. raise OfflineImapError("No remote host for repository "
  145. "'%s' specified." % self,
  146. OfflineImapError.ERROR.REPO)
  147. def get_remote_identity(self):
  148. """Remote identity is used for certain SASL mechanisms
  149. (currently -- PLAIN) to inform server about the ID
  150. we want to authorize as instead of our login name."""
  151. identity = self.getconf('remote_identity', default=None)
  152. if identity is not None:
  153. identity = identity.encode('UTF-8')
  154. return identity
  155. def get_auth_mechanisms(self):
  156. """
  157. Get the AUTH mechanisms. We have (ranged from the strongest to weakest)
  158. these methods: "GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"
  159. Returns: The supported AUTH Methods
  160. """
  161. supported = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
  162. # Mechanisms are ranged from the strongest to the
  163. # weakest ones.
  164. # TODO: we need DIGEST-MD5, it must come before CRAM-MD5
  165. # due to the chosen-plaintext resistance.
  166. default = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
  167. mechs = self.getconflist('auth_mechanisms', r',\s*',
  168. default)
  169. for mech in mechs:
  170. if mech not in supported:
  171. raise OfflineImapError("Repository %s: " % self +
  172. "unknown authentication mechanism '%s'"
  173. % mech, OfflineImapError.ERROR.REPO)
  174. self.ui.debug('imap', "Using authentication mechanisms %s" % mechs)
  175. return mechs
  176. def getuser(self):
  177. """
  178. Returns the remoteusereval or remoteuser or netrc user value.
  179. Returns: Returns the remoteusereval or remoteuser or netrc user value.
  180. """
  181. if self.config.has_option(self.getsection(), 'remoteusereval'):
  182. user = self.getconf('remoteusereval')
  183. if user is not None:
  184. l_user = self.localeval.eval(user)
  185. # We need a str username
  186. if isinstance(l_user, bytes):
  187. return l_user.decode(encoding='utf-8')
  188. elif isinstance(l_user, str):
  189. return l_user
  190. # If is not bytes or str, we have a problem
  191. raise OfflineImapError("Could not get a right username format for"
  192. " repository %s. Type found: %s. "
  193. "Please, open a bug." %
  194. (self.name, type(l_user)),
  195. OfflineImapError.ERROR.FOLDER)
  196. if self.config.has_option(self.getsection(), 'remoteuser'):
  197. # Assume the configuration file to be UTF-8 encoded so we must not
  198. # encode this string again.
  199. user = self.getconf('remoteuser')
  200. if user is not None:
  201. return user
  202. try:
  203. netrcentry = netrc.netrc().authenticators(self.gethost())
  204. except IOError as inst:
  205. if inst.errno != errno.ENOENT:
  206. raise
  207. else:
  208. if netrcentry:
  209. return netrcentry[0]
  210. try:
  211. netrcentry = netrc.netrc('/etc/netrc')\
  212. .authenticators(self.gethost())
  213. except IOError as inst:
  214. if inst.errno not in (errno.ENOENT, errno.EACCES):
  215. raise
  216. else:
  217. if netrcentry:
  218. return netrcentry[0]
  219. def getport(self):
  220. """
  221. Returns remoteporteval value or None if not found.
  222. Returns: Returns remoteporteval int value or None if not found.
  223. """
  224. port = None
  225. if self.config.has_option(self.getsection(), 'remoteporteval'):
  226. port = self.getconf('remoteporteval')
  227. if port is not None:
  228. return self.localeval.eval(port)
  229. return self.getconfint('remoteport', None)
  230. def getipv6(self):
  231. """
  232. Returns if IPv6 is set. If not set, then return None
  233. Returns: Boolean flag if IPv6 is set.
  234. """
  235. return self.getconfboolean('ipv6', None)
  236. def getssl(self):
  237. """
  238. Get the boolean SSL value. Default is True, used if not found.
  239. Returns: Get the boolean SSL value. Default is True
  240. """
  241. return self.getconfboolean('ssl', True)
  242. def getsslclientcert(self):
  243. """
  244. Return the SSL client cert (sslclientcert) or None if not found
  245. Returns: SSL client key (sslclientcert) or None if not found
  246. """
  247. xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
  248. return self.getconf_xform('sslclientcert', xforms, None)
  249. def getsslclientkey(self):
  250. """
  251. Return the SSL client key (sslclientkey) or None if not found
  252. Returns: SSL client key (sslclientkey) or None if not found
  253. """
  254. xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
  255. return self.getconf_xform('sslclientkey', xforms, None)
  256. def getsslcacertfile(self):
  257. """Determines CA bundle.
  258. Returns path to the CA bundle. It is either explicitely specified
  259. or requested via "OS-DEFAULT" value (and we will search known
  260. locations for the current OS and distribution).
  261. If search via "OS-DEFAULT" route yields nothing, we will throw an
  262. exception to make our callers distinguish between not specified
  263. value and non-existent default CA bundle.
  264. It is also an error to specify non-existent file via configuration:
  265. it will error out later, but, perhaps, with less verbose explanation,
  266. so we will also throw an exception. It is consistent with
  267. the above behaviour, so any explicitely-requested configuration
  268. that doesn't result in an existing file will give an exception.
  269. """
  270. xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
  271. cacertfile = self.getconf_xform('sslcacertfile', xforms, None)
  272. # Can't use above cacertfile because of abspath.
  273. if self.getconf('sslcacertfile', None) == "OS-DEFAULT":
  274. cacertfile = get_os_sslcertfile()
  275. if cacertfile is None:
  276. searchpath = get_os_sslcertfile_searchpath()
  277. if searchpath:
  278. reason = "Default CA bundle was requested, " \
  279. "but no existing locations available. " \
  280. "Tried %s." % (", ".join(searchpath))
  281. else:
  282. reason = "Default CA bundle was requested, " \
  283. "but OfflineIMAP doesn't know any for your " \
  284. "current operating system."
  285. raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
  286. if cacertfile is None:
  287. return None
  288. if not os.path.isfile(cacertfile):
  289. reason = "CA certfile for repository '%s' couldn't be found. " \
  290. "No such file: '%s'" % (self.name, cacertfile)
  291. raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
  292. return cacertfile
  293. def gettlslevel(self):
  294. """
  295. Returns the TLS level (tls_level). If not set, returns 'tls_compat'
  296. Returns: TLS level (tls_level). If not set, returns 'tls_compat'
  297. """
  298. return self.getconf('tls_level', 'tls_compat')
  299. def getsslversion(self):
  300. """
  301. Returns the SSL version. If not set, returns None.
  302. Returns: SSL version. If not set, returns None.
  303. """
  304. return self.getconf('ssl_version', None)
  305. def getstarttls(self):
  306. """
  307. Get the value of starttls. If not set, returns True
  308. Returns: Value of starttls. If not set, returns True
  309. """
  310. return self.getconfboolean('starttls', True)
  311. def get_ssl_fingerprint(self):
  312. """Return array of possible certificate fingerprints.
  313. Configuration item cert_fingerprint can contain multiple
  314. comma-separated fingerprints in hex form."""
  315. value = self.getconf('cert_fingerprint', "")
  316. return [f.strip().lower().replace(":", "")
  317. for f in value.split(',') if f]
  318. def setoauth2_request_url(self, url):
  319. """
  320. Set the OAUTH2 URL request.
  321. Args:
  322. url: OAUTH2 URL request
  323. Returns: None
  324. """
  325. self.oauth2_request_url = url
  326. def getoauth2_request_url(self):
  327. """
  328. Returns the OAUTH2 URL request from configuration (oauth2_request_url).
  329. If it is not found, then returns None
  330. Returns: OAUTH2 URL request (oauth2_request_url)
  331. """
  332. if self.oauth2_request_url is not None: # Use cached value if possible.
  333. return self.oauth2_request_url
  334. self.setoauth2_request_url(self.getconf('oauth2_request_url', None))
  335. return self.oauth2_request_url
  336. def getoauth2_refresh_token(self):
  337. """
  338. Get the OAUTH2 refresh token from the configuration
  339. (oauth2_refresh_token)
  340. If the access token is not found, then returns None.
  341. Returns: OAUTH2 refresh token (oauth2_refresh_token)
  342. """
  343. refresh_token = self.getconf('oauth2_refresh_token', None)
  344. if refresh_token is None:
  345. refresh_token = self.localeval.eval(
  346. self.getconf('oauth2_refresh_token_eval', "None")
  347. )
  348. if refresh_token is not None:
  349. refresh_token = refresh_token.strip("\n")
  350. return refresh_token
  351. def getoauth2_access_token(self):
  352. """
  353. Get the OAUTH2 access token from the configuration (oauth2_access_token)
  354. If the access token is not found, then returns None.
  355. Returns: OAUTH2 access token (oauth2_access_token)
  356. """
  357. access_token = self.getconf('oauth2_access_token', None)
  358. if access_token is None:
  359. access_token = self.localeval.eval(
  360. self.getconf('oauth2_access_token_eval', "None")
  361. )
  362. if access_token is not None:
  363. access_token = access_token.strip("\n")
  364. return access_token
  365. def getoauth2_client_id(self):
  366. """
  367. Get the OAUTH2 client id (oauth2_client_id) from the configuration.
  368. If not found, returns None
  369. Returns: OAUTH2 client id (oauth2_client_id)
  370. """
  371. client_id = self.getconf('oauth2_client_id', None)
  372. if client_id is None:
  373. client_id = self.localeval.eval(
  374. self.getconf('oauth2_client_id_eval', "None")
  375. )
  376. if client_id is not None:
  377. client_id = client_id.strip("\n")
  378. return client_id
  379. def getoauth2_client_secret(self):
  380. """
  381. Get the OAUTH2 client secret (oauth2_client_secret) from the
  382. configuration. If it is not found, then returns None.
  383. Returns: OAUTH2 client secret
  384. """
  385. client_secret = self.getconf('oauth2_client_secret', None)
  386. if client_secret is None:
  387. client_secret = self.localeval.eval(
  388. self.getconf('oauth2_client_secret_eval', "None")
  389. )
  390. if client_secret is not None:
  391. client_secret = client_secret.strip("\n")
  392. return client_secret
  393. def getpreauthtunnel(self):
  394. """
  395. Get the value of preauthtunnel. If not found, then returns None.
  396. Returns: Returns preauthtunnel value. If not found, returns None.
  397. """
  398. return self.getconf('preauthtunnel', None)
  399. def gettransporttunnel(self):
  400. """
  401. Get the value of transporttunnel. If not found, then returns None.
  402. Returns: Returns transporttunnel value. If not found, returns None.
  403. """
  404. return self.getconf('transporttunnel', None)
  405. def getreference(self):
  406. """
  407. Get the reference value in the configuration. If the value is not found
  408. then returns a double quote ("") as string.
  409. Returns: The reference variable. If not set, then returns '""'
  410. """
  411. return self.getconf('reference', '""')
  412. def getdecodefoldernames(self):
  413. """
  414. Get the boolean value of decodefoldernames configuration variable,
  415. if the value is not found, returns False.
  416. Returns: Boolean value of decodefoldernames, else False
  417. """
  418. return self.getconfboolean('decodefoldernames', False)
  419. def getidlefolders(self):
  420. """
  421. Get the list of idlefolders from configuration. If the value is not
  422. found, returns an empty list.
  423. Returns: A list of idle folders
  424. """
  425. if self.idlefolders is None:
  426. self.idlefolders = self.localeval.eval(
  427. self.getconf('idlefolders', '[]')
  428. )
  429. return self.idlefolders
  430. def getmaxconnections(self):
  431. """
  432. Get the maxconnections configuration value from configuration.
  433. If the value is not set, returns 1 connection
  434. Returns: Integer value of maxconnections configuration variable, else 1
  435. """
  436. num1 = len(self.getidlefolders())
  437. num2 = self.getconfint('maxconnections', 1)
  438. return max(num1, num2)
  439. def getexpunge(self):
  440. """
  441. Get the expunge configuration value from configuration.
  442. If the value is not set in the configuration, then returns True
  443. Returns: Boolean value of expunge configuration variable
  444. """
  445. return self.getconfboolean('expunge', True)
  446. def getpassword(self, ignore_keyring=False):
  447. """Return the IMAP password for this repository.
  448. It tries to get passwords in the following order:
  449. 1. evaluate Repository 'remotepasseval'
  450. 2. read password from Repository 'remotepass'
  451. 3. read password from file specified in Repository 'remotepassfile'
  452. 4. read password from ~/.netrc
  453. 5. read password from /etc/netrc
  454. 6. read password from keyring
  455. On success we return the password.
  456. If all strategies fail we return None."""
  457. # 1. Evaluate Repository 'remotepasseval'.
  458. passwd = self.getconf('remotepasseval', None)
  459. if passwd is not None:
  460. l_pass = self.localeval.eval(passwd)
  461. # We need a str password
  462. if isinstance(l_pass, bytes):
  463. return l_pass.decode(encoding='utf-8')
  464. elif isinstance(l_pass, str):
  465. return l_pass
  466. # If is not bytes or str, we have a problem
  467. raise OfflineImapError("Could not get a right password format for"
  468. " repository %s. Type found: %s. "
  469. "Please, open a bug." %
  470. (self.name, type(l_pass)),
  471. OfflineImapError.ERROR.FOLDER)
  472. # 2. Read password from Repository 'remotepass'.
  473. password = self.getconf('remotepass', None)
  474. if password is not None:
  475. # Assume the configuration file to be UTF-8 encoded so we must not
  476. # encode this string again.
  477. return password
  478. # 3. Read password from file specified in Repository 'remotepassfile'.
  479. passfile = self.getconf('remotepassfile', None)
  480. if passfile is not None:
  481. file_desc = open(os.path.expanduser(passfile), 'r',
  482. encoding='utf-8')
  483. password = file_desc.readline().strip()
  484. file_desc.close()
  485. # We need a str password
  486. if isinstance(password, bytes):
  487. return password.decode(encoding='utf-8')
  488. elif isinstance(password, str):
  489. return password
  490. # If is not bytes or str, we have a problem
  491. raise OfflineImapError("Could not get a right password format for"
  492. " repository %s. Type found: %s. "
  493. "Please, open a bug." %
  494. (self.name, type(password)),
  495. OfflineImapError.ERROR.FOLDER)
  496. # 4. Read password from ~/.netrc.
  497. try:
  498. netrcentry = netrc.netrc().authenticators(self.gethost())
  499. except IOError as inst:
  500. if inst.errno != errno.ENOENT:
  501. raise
  502. else:
  503. if netrcentry:
  504. user = self.getuser()
  505. if user is None or user == netrcentry[0]:
  506. return netrcentry[2]
  507. # 5. Read password from /etc/netrc.
  508. try:
  509. netrcentry = netrc.netrc('/etc/netrc')\
  510. .authenticators(self.gethost())
  511. except IOError as inst:
  512. if inst.errno not in (errno.ENOENT, errno.EACCES):
  513. raise
  514. else:
  515. if netrcentry:
  516. user = self.getuser()
  517. if user is None or user == netrcentry[0]:
  518. return netrcentry[2]
  519. # 6. Read password from keyring as the last option
  520. if not ignore_keyring:
  521. import keyring
  522. return keyring.get_password(self.gethost(), self.getuser())
  523. return None
  524. def updatepassword(self, password):
  525. """
  526. This function update provided password into system keyring.
  527. None means to remove it.
  528. """
  529. import keyring
  530. if password is None:
  531. keyring.delete_password(self.gethost(), self.getuser())
  532. else:
  533. keyring.set_password(self.gethost(), self.getuser(), password)
  534. def getfolder(self, foldername, decode=True):
  535. """Return instance of OfflineIMAP representative folder."""
  536. return self.getfoldertype()(self.imapserver, foldername, self, decode)
  537. def getfoldertype(self):
  538. """
  539. This function returns the folder type, in this case
  540. folder.IMAP.IMAPFolder
  541. Returns: folder.IMAP.IMAPFolder
  542. """
  543. return folder.IMAP.IMAPFolder
  544. def connect(self):
  545. imapobj = self.imapserver.acquireconnection()
  546. self.imapserver.releaseconnection(imapobj)
  547. def forgetfolders(self):
  548. self.folders = None
  549. def getfolders(self):
  550. """Return a list of instances of OfflineIMAP representative folder."""
  551. if self.folders is not None:
  552. return self.folders
  553. retval = []
  554. imapobj = self.imapserver.acquireconnection()
  555. # check whether to list all folders, or subscribed only
  556. listfunction = imapobj.list
  557. if self.getconfboolean('subscribedonly', False):
  558. listfunction = imapobj.lsub
  559. try:
  560. result, listresult = \
  561. listfunction(directory=self.imapserver.reference, pattern='"*"')
  562. if result != 'OK':
  563. raise OfflineImapError("Could not list the folders for"
  564. " repository %s. Server responded: %s" %
  565. (self.name, str(listresult)),
  566. OfflineImapError.ERROR.FOLDER)
  567. finally:
  568. self.imapserver.releaseconnection(imapobj)
  569. for fldr in listresult:
  570. if fldr is None or (isinstance(fldr, str) and fldr == ''):
  571. # Bug in imaplib: empty strings in results from
  572. # literals. TODO: still relevant?
  573. continue
  574. try:
  575. flags, delim, name = imaputil.imapsplit(fldr)
  576. except ValueError:
  577. self.ui.error(
  578. "could not correctly parse server response; got: %s" % fldr)
  579. raise
  580. flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
  581. if '\\noselect' in flaglist:
  582. continue
  583. retval.append(self.getfoldertype()(self.imapserver, name,
  584. self))
  585. # Add all folderincludes
  586. if len(self.folderincludes):
  587. imapobj = self.imapserver.acquireconnection()
  588. try:
  589. for foldername in self.folderincludes:
  590. try:
  591. imapobj.select(imaputil.utf8_IMAP(imaputil.foldername_to_imapname(foldername)),
  592. readonly=True)
  593. except OfflineImapError as exc:
  594. # couldn't select this folderinclude, so ignore folder.
  595. if exc.severity > OfflineImapError.ERROR.FOLDER:
  596. raise
  597. self.ui.error(exc, exc_info()[2],
  598. 'Invalid folderinclude:')
  599. continue
  600. retval.append(self.getfoldertype()(
  601. self.imapserver, foldername, self, decode=False))
  602. finally:
  603. self.imapserver.releaseconnection(imapobj)
  604. if self.foldersort is None:
  605. # default sorting by case insensitive transposed name
  606. retval.sort(key=lambda x: str.lower(x.getvisiblename()))
  607. else:
  608. # do foldersort in a python3-compatible way
  609. # http://bytes.com/topic/python/answers/ \
  610. # 844614-python-3-sorting-comparison-function
  611. def cmp2key(mycmp):
  612. """Converts a cmp= function into a key= function
  613. We need to keep cmp functions for backward compatibility"""
  614. class K:
  615. """
  616. Class to compare getvisiblename() between two objects.
  617. """
  618. def __init__(self, obj, *args):
  619. self.obj = obj
  620. def __cmp__(self, other):
  621. return mycmp(self.obj.getvisiblename(),
  622. other.obj.getvisiblename())
  623. def __lt__(self, other):
  624. return self.__cmp__(other) < 0
  625. def __le__(self, other):
  626. return self.__cmp__(other) <= 0
  627. def __gt__(self, other):
  628. return self.__cmp__(other) > 0
  629. def __ge__(self, other):
  630. return self.__cmp__(other) >= 0
  631. def __eq__(self, other):
  632. return self.__cmp__(other) == 0
  633. def __ne__(self, other):
  634. return self.__cmp__(other) != 0
  635. return K
  636. retval.sort(key=cmp2key(self.foldersort))
  637. self.folders = retval
  638. return self.folders
  639. def deletefolder(self, foldername):
  640. """Delete a folder on the IMAP server."""
  641. # Folder names with spaces requires quotes
  642. foldername = imaputil.foldername_to_imapname(foldername)
  643. if self.account.utf_8_support:
  644. foldername = imaputil.utf8_IMAP(foldername)
  645. imapobj = self.imapserver.acquireconnection()
  646. try:
  647. result = imapobj.delete(foldername)
  648. if result[0] != 'OK':
  649. msg = "Folder '%s'[%s] could not be deleted. "\
  650. "Server responded: %s" % (foldername, self, str(result))
  651. raise OfflineImapError(msg, OfflineImapError.ERROR.FOLDER)
  652. finally:
  653. self.imapserver.releaseconnection(imapobj)
  654. def makefolder(self, foldername):
  655. """
  656. Create a folder on the IMAP server
  657. This will not update the list cached in :meth:`getfolders`. You
  658. will need to invoke :meth:`forgetfolders` to force new caching
  659. when you are done creating folders yourself.
  660. Args:
  661. foldername: Full path of the folder to be created
  662. Returns: None
  663. """
  664. if foldername == '':
  665. return
  666. if self.getreference() != '""':
  667. foldername = self.getreference() + self.getsep() + foldername
  668. if not foldername: # Create top level folder as folder separator.
  669. foldername = self.getsep()
  670. self.makefolder_single(foldername)
  671. return
  672. parts = foldername.split(self.getsep())
  673. folder_paths = [self.getsep().join(parts[:n + 1])
  674. for n in range(len(parts))]
  675. for folder_path in folder_paths:
  676. if not imaputil.foldername_to_imapname(folder_path) in [ f.getfullIMAPname() for f in self.getfolders() ] :
  677. try:
  678. self.makefolder_single(folder_path)
  679. except OfflineImapError as exc:
  680. if '[ALREADYEXISTS]' not in exc.reason:
  681. raise
  682. def makefolder_single(self, foldername):
  683. """
  684. Create a IMAP folder.
  685. Args:
  686. foldername: Folder's name to create
  687. Returns: None
  688. """
  689. self.ui.makefolder(self, foldername)
  690. if self.account.dryrun:
  691. return
  692. imapobj = self.imapserver.acquireconnection()
  693. try:
  694. # Folder names with spaces requires quotes
  695. foldername = imaputil.foldername_to_imapname(foldername)
  696. if self.account.utf_8_support:
  697. foldername = imaputil.utf8_IMAP(foldername)
  698. result = imapobj.create(foldername)
  699. if result[0] != 'OK':
  700. msg = "Folder '%s'[%s] could not be created. "\
  701. "Server responded: %s" % (foldername, self, str(result))
  702. raise OfflineImapError(msg, OfflineImapError.ERROR.FOLDER)
  703. finally:
  704. self.imapserver.releaseconnection(imapobj)
  705. if result[0] == 'OK':
  706. self.forgetfolders()
  707. self.getfolders()
  708. class MappedIMAPRepository(IMAPRepository):
  709. """
  710. This subclass of IMAPRepository includes only the method
  711. getfoldertype modified that returns folder.UIDMaps.MappedIMAPFolder
  712. instead of folder.IMAP.IMAPFolder
  713. """
  714. def getfoldertype(self):
  715. return folder.UIDMaps.MappedIMAPFolder