# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2002 Ben Escoto <ben@emerose.org>
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
# Copyright 2008 Ian Barton <ian@manor-farm.org>
#
# This file is part of duplicity.
#
# Duplicity is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# Duplicity is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import imaplib
import re
import os
import time
import socket
import StringIO
import rfc822
import getpass
import email
import duplicity.backend
from duplicity import globals
from duplicity import log
from duplicity.errors import * # @UnusedWildImport
[docs]class ImapBackend(duplicity.backend.Backend):
def __init__(self, parsed_url):
duplicity.backend.Backend.__init__(self, parsed_url)
log.Debug("I'm %s (scheme %s) connecting to %s as %s" %
(self.__class__.__name__, parsed_url.scheme, parsed_url.hostname, parsed_url.username))
# Store url for reconnection on error
self.url = parsed_url
# Set the username
if (parsed_url.username is None):
username = raw_input('Enter account userid: ')
else:
username = parsed_url.username
# Set the password
if (not parsed_url.password):
if 'IMAP_PASSWORD' in os.environ:
password = os.environ.get('IMAP_PASSWORD')
else:
password = getpass.getpass("Enter account password: ")
else:
password = parsed_url.password
self.username = username
self.password = password
self.resetConnection()
[docs] def resetConnection(self):
parsed_url = self.url
try:
imap_server = os.environ['IMAP_SERVER']
except KeyError:
imap_server = parsed_url.hostname
# Try to close the connection cleanly
try:
self.conn.close()
except Exception:
pass
if (parsed_url.scheme == "imap"):
cl = imaplib.IMAP4
self.conn = cl(imap_server, 143)
elif (parsed_url.scheme == "imaps"):
cl = imaplib.IMAP4_SSL
self.conn = cl(imap_server, 993)
log.Debug("Type of imap class: %s" % (cl.__name__))
self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1)
# Login
if (not(globals.imap_full_address)):
self.conn.login(self.username, self.password)
self.conn.select(globals.imap_mailbox)
log.Info("IMAP connected")
else:
self.conn.login(self.username + "@" + parsed_url.hostname, self.password)
self.conn.select(globals.imap_mailbox)
log.Info("IMAP connected")
[docs] def prepareBody(self, f, rname):
mp = email.MIMEMultipart.MIMEMultipart()
# I am going to use the remote_dir as the From address so that
# multiple archives can be stored in an IMAP account and can be
# accessed separately
mp["From"] = self.remote_dir
mp["Subject"] = rname
a = email.MIMEBase.MIMEBase("application", "binary")
a.set_payload(f.read())
email.Encoders.encode_base64(a)
mp.attach(a)
return mp.as_string()
def _put(self, source_path, remote_filename):
f = source_path.open("rb")
allowedTimeout = globals.timeout
if (allowedTimeout == 0):
# Allow a total timeout of 1 day
allowedTimeout = 2880
while allowedTimeout > 0:
try:
self.conn.select(remote_filename)
body = self.prepareBody(f, remote_filename)
# If we don't select the IMAP folder before
# append, the message goes into the INBOX.
self.conn.select(globals.imap_mailbox)
self.conn.append(globals.imap_mailbox, None, None, body)
break
except (imaplib.IMAP4.abort, socket.error, socket.sslerror):
allowedTimeout -= 1
log.Info("Error saving '%s', retrying in 30s " % remote_filename)
time.sleep(30)
while allowedTimeout > 0:
try:
self.resetConnection()
break
except (imaplib.IMAP4.abort, socket.error, socket.sslerror):
allowedTimeout -= 1
log.Info("Error reconnecting, retrying in 30s ")
time.sleep(30)
log.Info("IMAP mail with '%s' subject stored" % remote_filename)
def _get(self, remote_filename, local_path):
allowedTimeout = globals.timeout
if (allowedTimeout == 0):
# Allow a total timeout of 1 day
allowedTimeout = 2880
while allowedTimeout > 0:
try:
self.conn.select(globals.imap_mailbox)
(result, list) = self.conn.search(None, 'Subject', remote_filename)
if result != "OK":
raise Exception(list[0])
# check if there is any result
if list[0] == '':
raise Exception("no mail with subject %s")
(result, list) = self.conn.fetch(list[0], "(RFC822)")
if result != "OK":
raise Exception(list[0])
rawbody = list[0][1]
p = email.Parser.Parser()
m = p.parsestr(rawbody)
mp = m.get_payload(0)
body = mp.get_payload(decode=True)
break
except (imaplib.IMAP4.abort, socket.error, socket.sslerror):
allowedTimeout -= 1
log.Info("Error loading '%s', retrying in 30s " % remote_filename)
time.sleep(30)
while allowedTimeout > 0:
try:
self.resetConnection()
break
except (imaplib.IMAP4.abort, socket.error, socket.sslerror):
allowedTimeout -= 1
log.Info("Error reconnecting, retrying in 30s ")
time.sleep(30)
tfile = local_path.open("wb")
tfile.write(body)
local_path.setdata()
log.Info("IMAP mail with '%s' subject fetched" % remote_filename)
def _list(self):
ret = []
(result, list) = self.conn.select(globals.imap_mailbox)
if result != "OK":
raise BackendException(list[0])
# Going to find all the archives which have remote_dir in the From
# address
# Search returns an error if you haven't selected an IMAP folder.
(result, list) = self.conn.search(None, 'FROM', self.remote_dir)
if result != "OK":
raise Exception(list[0])
if list[0] == '':
return ret
nums = list[0].strip().split(" ")
set = "%s:%s" % (nums[0], nums[-1])
(result, list) = self.conn.fetch(set, "(BODY[HEADER])")
if result != "OK":
raise Exception(list[0])
for msg in list:
if (len(msg) == 1):
continue
io = StringIO.StringIO(msg[1]) # pylint: disable=unsubscriptable-object
m = rfc822.Message(io)
subj = m.getheader("subject")
header_from = m.getheader("from")
# Catch messages with empty headers which cause an exception.
if (not (header_from is None)):
if (re.compile("^" + self.remote_dir + "$").match(header_from)):
ret.append(subj)
log.Info("IMAP LIST: %s %s" % (subj, header_from))
return ret
[docs] def imapf(self, fun, *args):
(ret, list) = fun(*args)
if ret != "OK":
raise Exception(list[0])
return list
[docs] def delete_single_mail(self, i):
self.imapf(self.conn.store, i, "+FLAGS", '\\DELETED')
[docs] def expunge(self):
list = self.imapf(self.conn.expunge)
def _delete_list(self, filename_list):
for filename in filename_list:
list = self.imapf(self.conn.search, None, "(SUBJECT %s)" % filename)
list = list[0].split()
if len(list) > 0 and list[0] != "":
self.delete_single_mail(list[0])
log.Notice("marked %s to be deleted" % filename)
self.expunge()
log.Notice("IMAP expunged %s files" % len(filename_list))
def _close(self):
self.conn.select(globals.imap_mailbox)
self.conn.close()
self.conn.logout()
duplicity.backend.register_backend("imap", ImapBackend)
duplicity.backend.register_backend("imaps", ImapBackend)
duplicity.backend.uses_netloc.extend(['imap', 'imaps'])