mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
E-mail sending: calibre will now send atmost one email every minute. ALso if sending an email fails, it will be automatically retired once, after a minute. Finally, email sending is now a normal job and can be viewed in the jobs list. Fixes #2473 (Feature request: Show email sending as job in queue)
This commit is contained in:
parent
2999279a51
commit
ac8dc12785
@ -3,11 +3,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
# Imports {{{
|
# Imports {{{
|
||||||
import os, traceback, Queue, time, socket, cStringIO, re, sys
|
import os, traceback, Queue, time, cStringIO, re, sys
|
||||||
from threading import Thread, RLock
|
from threading import Thread
|
||||||
from itertools import repeat
|
|
||||||
from functools import partial
|
|
||||||
from binascii import unhexlify
|
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
|
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
|
||||||
Qt, pyqtSignal, QDialog, QMessageBox
|
Qt, pyqtSignal, QDialog, QMessageBox
|
||||||
@ -25,8 +22,6 @@ from calibre.ebooks.metadata import authors_to_string
|
|||||||
from calibre import preferred_encoding, prints, force_unicode
|
from calibre import preferred_encoding, prints, force_unicode
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.devices.errors import FreeSpaceError
|
from calibre.devices.errors import FreeSpaceError
|
||||||
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
|
||||||
config as email_config
|
|
||||||
from calibre.devices.apple.driver import ITUNES_ASYNC
|
from calibre.devices.apple.driver import ITUNES_ASYNC
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
||||||
from calibre.ebooks.metadata.meta import set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
@ -591,64 +586,6 @@ class DeviceMenu(QMenu): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Emailer(Thread): # {{{
|
|
||||||
|
|
||||||
def __init__(self, timeout=60):
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.setDaemon(True)
|
|
||||||
self.job_lock = RLock()
|
|
||||||
self.jobs = []
|
|
||||||
self._run = True
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while self._run:
|
|
||||||
job = None
|
|
||||||
with self.job_lock:
|
|
||||||
if self.jobs:
|
|
||||||
job = self.jobs[0]
|
|
||||||
self.jobs = self.jobs[1:]
|
|
||||||
if job is not None:
|
|
||||||
self._send_mails(*job)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._run = False
|
|
||||||
|
|
||||||
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
|
|
||||||
texts, attachment_names):
|
|
||||||
job = (jobnames, callback, attachments, to_s, subjects, texts,
|
|
||||||
attachment_names)
|
|
||||||
with self.job_lock:
|
|
||||||
self.jobs.append(job)
|
|
||||||
|
|
||||||
def _send_mails(self, jobnames, callback, attachments,
|
|
||||||
to_s, subjects, texts, attachment_names):
|
|
||||||
opts = email_config().parse()
|
|
||||||
opts.verbose = 3 if os.environ.get('CALIBRE_DEBUG_EMAIL', False) else 0
|
|
||||||
from_ = opts.from_
|
|
||||||
if not from_:
|
|
||||||
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
|
||||||
results = []
|
|
||||||
for i, jobname in enumerate(jobnames):
|
|
||||||
try:
|
|
||||||
msg = compose_mail(from_, to_s[i], texts[i], subjects[i],
|
|
||||||
open(attachments[i], 'rb'),
|
|
||||||
attachment_name = attachment_names[i])
|
|
||||||
efrom, eto = map(extract_email_address, (from_, to_s[i]))
|
|
||||||
eto = [eto]
|
|
||||||
sendmail(msg, efrom, eto, localhost=None,
|
|
||||||
verbose=opts.verbose,
|
|
||||||
timeout=self.timeout, relay=opts.relay_host,
|
|
||||||
username=opts.relay_username,
|
|
||||||
password=unhexlify(opts.relay_password), port=opts.relay_port,
|
|
||||||
encryption=opts.encryption)
|
|
||||||
results.append([jobname, None, None])
|
|
||||||
except Exception, e:
|
|
||||||
results.append([jobname, e, traceback.format_exc()])
|
|
||||||
callback(results)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class DeviceMixin(object): # {{{
|
class DeviceMixin(object): # {{{
|
||||||
|
|
||||||
@ -656,8 +593,6 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||||
_('Error communicating with device'), ' ')
|
_('Error communicating with device'), ' ')
|
||||||
self.device_error_dialog.setModal(Qt.NonModal)
|
self.device_error_dialog.setModal(Qt.NonModal)
|
||||||
self.emailer = Emailer()
|
|
||||||
self.emailer.start()
|
|
||||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||||
self.job_manager, Dispatcher(self.status_bar.show_message))
|
self.job_manager, Dispatcher(self.status_bar.show_message))
|
||||||
self.device_manager.start()
|
self.device_manager.start()
|
||||||
@ -911,124 +846,6 @@ class DeviceMixin(object): # {{{
|
|||||||
fmts = [x.strip().lower() for x in fmts.split(',')]
|
fmts = [x.strip().lower() for x in fmts.split(',')]
|
||||||
self.send_by_mail(to, fmts, delete)
|
self.send_by_mail(to, fmts, delete)
|
||||||
|
|
||||||
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None,
|
|
||||||
do_auto_convert=True, specific_format=None):
|
|
||||||
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
|
||||||
if not ids or len(ids) == 0:
|
|
||||||
return
|
|
||||||
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
|
||||||
fmts, set_metadata=True,
|
|
||||||
specific_format=specific_format,
|
|
||||||
exclude_auto=do_auto_convert)
|
|
||||||
if do_auto_convert:
|
|
||||||
nids = list(set(ids).difference(_auto_ids))
|
|
||||||
ids = [i for i in ids if i in nids]
|
|
||||||
else:
|
|
||||||
_auto_ids = []
|
|
||||||
|
|
||||||
full_metadata = self.library_view.model().metadata_for(ids)
|
|
||||||
|
|
||||||
bad, remove_ids, jobnames = [], [], []
|
|
||||||
texts, subjects, attachments, attachment_names = [], [], [], []
|
|
||||||
for f, mi, id in zip(files, full_metadata, ids):
|
|
||||||
t = mi.title
|
|
||||||
if not t:
|
|
||||||
t = _('Unknown')
|
|
||||||
if f is None:
|
|
||||||
bad.append(t)
|
|
||||||
else:
|
|
||||||
remove_ids.append(id)
|
|
||||||
jobnames.append(u'%s:%s'%(id, t))
|
|
||||||
attachments.append(f)
|
|
||||||
subjects.append(_('E-book:')+ ' '+t)
|
|
||||||
a = authors_to_string(mi.authors if mi.authors else \
|
|
||||||
[_('Unknown')])
|
|
||||||
texts.append(_('Attached, you will find the e-book') + \
|
|
||||||
'\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
|
|
||||||
_('in the %s format.') %
|
|
||||||
os.path.splitext(f)[1][1:].upper())
|
|
||||||
prefix = ascii_filename(t+' - '+a)
|
|
||||||
if not isinstance(prefix, unicode):
|
|
||||||
prefix = prefix.decode(preferred_encoding, 'replace')
|
|
||||||
attachment_names.append(prefix + os.path.splitext(f)[1])
|
|
||||||
remove = remove_ids if delete_from_library else []
|
|
||||||
|
|
||||||
to_s = list(repeat(to, len(attachments)))
|
|
||||||
if attachments:
|
|
||||||
self.emailer.send_mails(jobnames,
|
|
||||||
Dispatcher(partial(self.emails_sent, remove=remove)),
|
|
||||||
attachments, to_s, subjects, texts, attachment_names)
|
|
||||||
self.status_bar.show_message(_('Sending email to')+' '+to, 3000)
|
|
||||||
|
|
||||||
auto = []
|
|
||||||
if _auto_ids != []:
|
|
||||||
for id in _auto_ids:
|
|
||||||
if specific_format == None:
|
|
||||||
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
|
|
||||||
formats = formats if formats != None else []
|
|
||||||
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
|
|
||||||
auto.append(id)
|
|
||||||
else:
|
|
||||||
bad.append(self.library_view.model().db.title(id, index_is_id=True))
|
|
||||||
else:
|
|
||||||
if specific_format in list(set(fmts).intersection(set(available_output_formats()))):
|
|
||||||
auto.append(id)
|
|
||||||
else:
|
|
||||||
bad.append(self.library_view.model().db.title(id, index_is_id=True))
|
|
||||||
|
|
||||||
if auto != []:
|
|
||||||
format = specific_format if specific_format in list(set(fmts).intersection(set(available_output_formats()))) else None
|
|
||||||
if not format:
|
|
||||||
for fmt in fmts:
|
|
||||||
if fmt in list(set(fmts).intersection(set(available_output_formats()))):
|
|
||||||
format = fmt
|
|
||||||
break
|
|
||||||
if format is None:
|
|
||||||
bad += auto
|
|
||||||
else:
|
|
||||||
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
|
|
||||||
if self.auto_convert_question(
|
|
||||||
_('Auto convert the following books before sending via '
|
|
||||||
'email?'), autos):
|
|
||||||
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format)
|
|
||||||
|
|
||||||
if bad:
|
|
||||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
|
||||||
d = warning_dialog(self, _('No suitable formats'),
|
|
||||||
_('Could not email the following books '
|
|
||||||
'as no suitable formats were found:'), bad)
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
def emails_sent(self, results, remove=[]):
|
|
||||||
errors, good = [], []
|
|
||||||
for jobname, exception, tb in results:
|
|
||||||
title = jobname.partition(':')[-1]
|
|
||||||
if exception is not None:
|
|
||||||
errors.append(list(map(force_unicode, [title, exception, tb])))
|
|
||||||
else:
|
|
||||||
good.append(title)
|
|
||||||
if errors:
|
|
||||||
errors = u'\n'.join([
|
|
||||||
u'%s\n\n%s\n%s\n' %
|
|
||||||
(title, e, tb) for \
|
|
||||||
title, e, tb in errors
|
|
||||||
])
|
|
||||||
error_dialog(self, _('Failed to email books'),
|
|
||||||
_('Failed to email the following books:'),
|
|
||||||
'%s'%errors, show=True
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
|
|
||||||
5000)
|
|
||||||
if remove:
|
|
||||||
try:
|
|
||||||
self.library_view.model().delete_books_by_id(remove)
|
|
||||||
except:
|
|
||||||
# Probably the user deleted the files, in any case, failing
|
|
||||||
# to delete the book is not catastrophic
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
|
|
||||||
def cover_to_thumbnail(self, data):
|
def cover_to_thumbnail(self, data):
|
||||||
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
||||||
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
||||||
@ -1037,36 +854,6 @@ class DeviceMixin(object): # {{{
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def email_news(self, id):
|
|
||||||
opts = email_config().parse()
|
|
||||||
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
|
|
||||||
for account, x in opts.accounts.items() if x[1]]
|
|
||||||
sent_mails = []
|
|
||||||
for account, fmts in accounts:
|
|
||||||
files, auto = self.library_view.model().\
|
|
||||||
get_preferred_formats_from_ids([id], fmts)
|
|
||||||
files = [f for f in files if f is not None]
|
|
||||||
if not files:
|
|
||||||
continue
|
|
||||||
attachment = files[0]
|
|
||||||
mi = self.library_view.model().db.get_metadata(id,
|
|
||||||
index_is_id=True)
|
|
||||||
to_s = [account]
|
|
||||||
subjects = [_('News:')+' '+mi.title]
|
|
||||||
texts = [_('Attached is the')+' '+mi.title]
|
|
||||||
attachment_names = [ascii_filename(mi.title)+os.path.splitext(attachment)[1]]
|
|
||||||
attachments = [attachment]
|
|
||||||
jobnames = ['%s:%s'%(id, mi.title)]
|
|
||||||
remove = [id] if config['delete_news_from_library_on_upload']\
|
|
||||||
else []
|
|
||||||
self.emailer.send_mails(jobnames,
|
|
||||||
Dispatcher(partial(self.emails_sent, remove=remove)),
|
|
||||||
attachments, to_s, subjects, texts, attachment_names)
|
|
||||||
sent_mails.append(to_s[0])
|
|
||||||
if sent_mails:
|
|
||||||
self.status_bar.show_message(_('Sent news to')+' '+\
|
|
||||||
', '.join(sent_mails), 3000)
|
|
||||||
|
|
||||||
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
|
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
|
||||||
if self.device_connected:
|
if self.device_connected:
|
||||||
settings = self.device_manager.device.settings()
|
settings = self.device_manager.device.settings()
|
||||||
|
321
src/calibre/gui2/email.py
Normal file
321
src/calibre/gui2/email.py
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, socket, time, cStringIO
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue
|
||||||
|
from binascii import unhexlify
|
||||||
|
from functools import partial
|
||||||
|
from itertools import repeat
|
||||||
|
|
||||||
|
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
||||||
|
config as email_config
|
||||||
|
from calibre.utils.filenames import ascii_filename
|
||||||
|
from calibre.utils.ipc.job import BaseJob
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.customize.ui import available_input_formats, available_output_formats
|
||||||
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.gui2 import config, Dispatcher, warning_dialog
|
||||||
|
|
||||||
|
class EmailJob(BaseJob): # {{{
|
||||||
|
|
||||||
|
def __init__(self, callback, description, attachment, aname, to, subject, text, job_manager):
|
||||||
|
BaseJob.__init__(self, description)
|
||||||
|
self.exception = None
|
||||||
|
self.job_manager = job_manager
|
||||||
|
self.email_args = (attachment, aname, to, subject, text)
|
||||||
|
self.email_sent_callback = callback
|
||||||
|
self.log_path = None
|
||||||
|
self._log_file = cStringIO.StringIO()
|
||||||
|
self._log_file.write(self.description.encode('utf-8') + '\n')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_file(self):
|
||||||
|
if self.log_path is not None:
|
||||||
|
return open(self.log_path, 'rb')
|
||||||
|
return cStringIO.StringIO(self._log_file.getvalue())
|
||||||
|
|
||||||
|
def start_work(self):
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.job_manager.changed_queue.put(self)
|
||||||
|
|
||||||
|
def job_done(self):
|
||||||
|
self.duration = time.time() - self.start_time
|
||||||
|
self.percent = 1
|
||||||
|
# Dump log onto disk
|
||||||
|
lf = PersistentTemporaryFile('email_log')
|
||||||
|
lf.write(self._log_file.getvalue())
|
||||||
|
lf.close()
|
||||||
|
self.log_path = lf.name
|
||||||
|
self._log_file.close()
|
||||||
|
self._log_file = None
|
||||||
|
|
||||||
|
self.job_manager.changed_queue.put(self)
|
||||||
|
|
||||||
|
def log_write(self, what):
|
||||||
|
self._log_file.write(what)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Emailer(Thread): # {{{
|
||||||
|
|
||||||
|
MAX_RETRIES = 1
|
||||||
|
RATE_LIMIT = 65 # seconds between connections to the SMTP server
|
||||||
|
|
||||||
|
def __init__(self, job_manager):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.jobs = Queue()
|
||||||
|
self.job_manager = job_manager
|
||||||
|
self._run = True
|
||||||
|
self.last_send_time = time.time() - self.RATE_LIMIT
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._run = False
|
||||||
|
self.jobs.put(None)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self._run:
|
||||||
|
try:
|
||||||
|
job = self.jobs.get()
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
if job is None or not self._run:
|
||||||
|
break
|
||||||
|
try_count = 0
|
||||||
|
failed, exc = False, None
|
||||||
|
job.start_work()
|
||||||
|
if job.kill_on_start:
|
||||||
|
job.log_write('Aborted\n')
|
||||||
|
job.failed = failed
|
||||||
|
job.killed = True
|
||||||
|
job.job_done()
|
||||||
|
continue
|
||||||
|
|
||||||
|
while try_count <= self.MAX_RETRIES:
|
||||||
|
failed = False
|
||||||
|
if try_count > 0:
|
||||||
|
job.log_write('\nRetrying in %d seconds...\n' %
|
||||||
|
self.RATE_LIMIT)
|
||||||
|
try:
|
||||||
|
self.sendmail(job)
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
if not self._run:
|
||||||
|
return
|
||||||
|
import traceback
|
||||||
|
failed = True
|
||||||
|
exc = e
|
||||||
|
job.log_write('\nSending failed...\n')
|
||||||
|
job.log_write(traceback.format_exc())
|
||||||
|
|
||||||
|
try_count += 1
|
||||||
|
|
||||||
|
if not self._run:
|
||||||
|
break
|
||||||
|
|
||||||
|
job.failed = failed
|
||||||
|
job.exception = exc
|
||||||
|
job.job_done()
|
||||||
|
job.email_sent_callback(job)
|
||||||
|
|
||||||
|
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
|
||||||
|
texts, attachment_names):
|
||||||
|
for name, attachment, to, subject, text, aname in zip(jobnames,
|
||||||
|
attachments, to_s, subjects, texts, attachment_names):
|
||||||
|
description = _('Email %s to %s') % (name, to)
|
||||||
|
job = EmailJob(callback, description, attachment, aname, to,
|
||||||
|
subject, text, self.job_manager)
|
||||||
|
self.job_manager.add_job(job)
|
||||||
|
self.jobs.put(job)
|
||||||
|
|
||||||
|
def sendmail(self, job):
|
||||||
|
while time.time() - self.last_send_time <= self.RATE_LIMIT:
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
opts = email_config().parse()
|
||||||
|
from_ = opts.from_
|
||||||
|
if not from_:
|
||||||
|
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||||
|
attachment, aname, to, subject, text = job.email_args
|
||||||
|
msg = compose_mail(from_, to, text, subject, open(attachment, 'rb'),
|
||||||
|
aname)
|
||||||
|
efrom, eto = map(extract_email_address, (from_, to))
|
||||||
|
eto = [eto]
|
||||||
|
sendmail(msg, efrom, eto, localhost=None,
|
||||||
|
verbose=1,
|
||||||
|
relay=opts.relay_host,
|
||||||
|
username=opts.relay_username,
|
||||||
|
password=unhexlify(opts.relay_password), port=opts.relay_port,
|
||||||
|
encryption=opts.encryption,
|
||||||
|
debug_output=partial(print, file=job._log_file))
|
||||||
|
finally:
|
||||||
|
self.last_send_time = time.time()
|
||||||
|
|
||||||
|
def email_news(self, mi, remove, get_fmts, done):
|
||||||
|
opts = email_config().parse()
|
||||||
|
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
|
||||||
|
for account, x in opts.accounts.items() if x[1]]
|
||||||
|
sent_mails = []
|
||||||
|
for i, x in enumerate(accounts):
|
||||||
|
account, fmts = x
|
||||||
|
files = get_fmts(fmts)
|
||||||
|
files = [f for f in files if f is not None]
|
||||||
|
if not files:
|
||||||
|
continue
|
||||||
|
attachment = files[0]
|
||||||
|
to_s = [account]
|
||||||
|
subjects = [_('News:')+' '+mi.title]
|
||||||
|
texts = [
|
||||||
|
_('Attached is the %s periodical downloaded by calibre.')
|
||||||
|
% (mi.title,)
|
||||||
|
]
|
||||||
|
attachment_names = [ascii_filename(mi.title)+os.path.splitext(attachment)[1]]
|
||||||
|
attachments = [attachment]
|
||||||
|
jobnames = [mi.title]
|
||||||
|
do_remove = []
|
||||||
|
if i == len(accounts) - 1:
|
||||||
|
do_remove = remove
|
||||||
|
self.send_mails(jobnames,
|
||||||
|
Dispatcher(partial(done, remove=do_remove)),
|
||||||
|
attachments, to_s, subjects, texts, attachment_names)
|
||||||
|
sent_mails.append(to_s[0])
|
||||||
|
return sent_mails
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailMixin(object): # {{{
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.emailer = Emailer(self.job_manager)
|
||||||
|
self.emailer.start()
|
||||||
|
|
||||||
|
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None,
|
||||||
|
do_auto_convert=True, specific_format=None):
|
||||||
|
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
||||||
|
if not ids or len(ids) == 0:
|
||||||
|
return
|
||||||
|
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
||||||
|
fmts, set_metadata=True,
|
||||||
|
specific_format=specific_format,
|
||||||
|
exclude_auto=do_auto_convert)
|
||||||
|
if do_auto_convert:
|
||||||
|
nids = list(set(ids).difference(_auto_ids))
|
||||||
|
ids = [i for i in ids if i in nids]
|
||||||
|
else:
|
||||||
|
_auto_ids = []
|
||||||
|
|
||||||
|
full_metadata = self.library_view.model().metadata_for(ids)
|
||||||
|
|
||||||
|
bad, remove_ids, jobnames = [], [], []
|
||||||
|
texts, subjects, attachments, attachment_names = [], [], [], []
|
||||||
|
for f, mi, id in zip(files, full_metadata, ids):
|
||||||
|
t = mi.title
|
||||||
|
if not t:
|
||||||
|
t = _('Unknown')
|
||||||
|
if f is None:
|
||||||
|
bad.append(t)
|
||||||
|
else:
|
||||||
|
remove_ids.append(id)
|
||||||
|
jobnames.append(t)
|
||||||
|
attachments.append(f)
|
||||||
|
subjects.append(_('E-book:')+ ' '+t)
|
||||||
|
a = authors_to_string(mi.authors if mi.authors else \
|
||||||
|
[_('Unknown')])
|
||||||
|
texts.append(_('Attached, you will find the e-book') + \
|
||||||
|
'\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
|
||||||
|
_('in the %s format.') %
|
||||||
|
os.path.splitext(f)[1][1:].upper())
|
||||||
|
prefix = ascii_filename(t+' - '+a)
|
||||||
|
if not isinstance(prefix, unicode):
|
||||||
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
|
attachment_names.append(prefix + os.path.splitext(f)[1])
|
||||||
|
remove = remove_ids if delete_from_library else []
|
||||||
|
|
||||||
|
to_s = list(repeat(to, len(attachments)))
|
||||||
|
if attachments:
|
||||||
|
self.emailer.send_mails(jobnames,
|
||||||
|
Dispatcher(partial(self.email_sent, remove=remove)),
|
||||||
|
attachments, to_s, subjects, texts, attachment_names)
|
||||||
|
self.status_bar.show_message(_('Sending email to')+' '+to, 3000)
|
||||||
|
|
||||||
|
auto = []
|
||||||
|
if _auto_ids != []:
|
||||||
|
for id in _auto_ids:
|
||||||
|
if specific_format == None:
|
||||||
|
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
|
||||||
|
formats = formats if formats != None else []
|
||||||
|
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
|
||||||
|
auto.append(id)
|
||||||
|
else:
|
||||||
|
bad.append(self.library_view.model().db.title(id, index_is_id=True))
|
||||||
|
else:
|
||||||
|
if specific_format in list(set(fmts).intersection(set(available_output_formats()))):
|
||||||
|
auto.append(id)
|
||||||
|
else:
|
||||||
|
bad.append(self.library_view.model().db.title(id, index_is_id=True))
|
||||||
|
|
||||||
|
if auto != []:
|
||||||
|
format = specific_format if specific_format in list(set(fmts).intersection(set(available_output_formats()))) else None
|
||||||
|
if not format:
|
||||||
|
for fmt in fmts:
|
||||||
|
if fmt in list(set(fmts).intersection(set(available_output_formats()))):
|
||||||
|
format = fmt
|
||||||
|
break
|
||||||
|
if format is None:
|
||||||
|
bad += auto
|
||||||
|
else:
|
||||||
|
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
|
||||||
|
if self.auto_convert_question(
|
||||||
|
_('Auto convert the following books before sending via '
|
||||||
|
'email?'), autos):
|
||||||
|
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format)
|
||||||
|
|
||||||
|
if bad:
|
||||||
|
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||||
|
d = warning_dialog(self, _('No suitable formats'),
|
||||||
|
_('Could not email the following books '
|
||||||
|
'as no suitable formats were found:'), bad)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def email_sent(self, job, remove=[]):
|
||||||
|
if job.failed:
|
||||||
|
self.job_exception(job, dialog_title=_('Failed to email book'))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.status_bar.show_message(job.description + ' ' + _('sent'),
|
||||||
|
5000)
|
||||||
|
if remove:
|
||||||
|
try:
|
||||||
|
self.library_view.model().delete_books_by_id(remove)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
# Probably the user deleted the files, in any case, failing
|
||||||
|
# to delete the book is not catastrophic
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def email_news(self, id_):
|
||||||
|
mi = self.library_view.model().db.get_metadata(id_,
|
||||||
|
index_is_id=True)
|
||||||
|
remove = [id_] if config['delete_news_from_library_on_upload'] \
|
||||||
|
else []
|
||||||
|
def get_fmts(fmts):
|
||||||
|
files, auto = self.library_view.model().\
|
||||||
|
get_preferred_formats_from_ids([id_], fmts)
|
||||||
|
return files
|
||||||
|
sent_mails = self.emailer.email_news(mi, remove,
|
||||||
|
get_fmts, self.email_sent)
|
||||||
|
if sent_mails:
|
||||||
|
self.status_bar.show_message(_('Sent news to')+' '+\
|
||||||
|
', '.join(sent_mails), 3000)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
@ -221,16 +221,27 @@ class JobManager(QAbstractTableModel):
|
|||||||
if job.duration is not None:
|
if job.duration is not None:
|
||||||
return error_dialog(view, _('Cannot kill job'),
|
return error_dialog(view, _('Cannot kill job'),
|
||||||
_('Job has already run')).exec_()
|
_('Job has already run')).exec_()
|
||||||
self.server.kill_job(job)
|
if isinstance(job, ParallelJob):
|
||||||
|
self.server.kill_job(job)
|
||||||
|
else:
|
||||||
|
job.kill_on_start = True
|
||||||
|
|
||||||
def kill_all_jobs(self):
|
def kill_all_jobs(self):
|
||||||
for job in self.jobs:
|
for job in self.jobs:
|
||||||
if isinstance(job, DeviceJob) or job.duration is not None:
|
if isinstance(job, DeviceJob) or job.duration is not None:
|
||||||
continue
|
continue
|
||||||
self.server.kill_job(job)
|
if isinstance(job, ParallelJob):
|
||||||
|
self.server.kill_job(job)
|
||||||
|
else:
|
||||||
|
job.kill_on_start = True
|
||||||
|
|
||||||
def terminate_all_jobs(self):
|
def terminate_all_jobs(self):
|
||||||
self.server.killall()
|
self.server.killall()
|
||||||
|
for job in self.jobs:
|
||||||
|
if isinstance(job, DeviceJob) or job.duration is not None:
|
||||||
|
continue
|
||||||
|
if not isinstance(job, ParallelJob):
|
||||||
|
job.kill_on_start = True
|
||||||
|
|
||||||
|
|
||||||
class ProgressBarDelegate(QAbstractItemDelegate):
|
class ProgressBarDelegate(QAbstractItemDelegate):
|
||||||
|
@ -34,6 +34,7 @@ from calibre.gui2.update import UpdateMixin
|
|||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2.layout import MainWindowMixin
|
from calibre.gui2.layout import MainWindowMixin
|
||||||
from calibre.gui2.device import DeviceMixin
|
from calibre.gui2.device import DeviceMixin
|
||||||
|
from calibre.gui2.email import EmailMixin
|
||||||
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
||||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
@ -88,7 +89,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
||||||
):
|
):
|
||||||
@ -141,6 +142,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
LayoutMixin.__init__(self)
|
LayoutMixin.__init__(self)
|
||||||
|
EmailMixin.__init__(self)
|
||||||
DeviceMixin.__init__(self)
|
DeviceMixin.__init__(self)
|
||||||
|
|
||||||
self.restriction_count_of_books_in_view = 0
|
self.restriction_count_of_books_in_view = 0
|
||||||
@ -434,7 +436,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def job_exception(self, job):
|
def job_exception(self, job, dialog_title=_('Conversion Error')):
|
||||||
if not hasattr(self, '_modeless_dialogs'):
|
if not hasattr(self, '_modeless_dialogs'):
|
||||||
self._modeless_dialogs = []
|
self._modeless_dialogs = []
|
||||||
minz = self.is_minimized_to_tray
|
minz = self.is_minimized_to_tray
|
||||||
@ -475,7 +477,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
if not minz:
|
if not minz:
|
||||||
d = error_dialog(self, _('Conversion Error'),
|
d = error_dialog(self, dialog_title,
|
||||||
_('<b>Failed</b>')+': '+unicode(job.description),
|
_('<b>Failed</b>')+': '+unicode(job.description),
|
||||||
det_msg=job.details)
|
det_msg=job.details)
|
||||||
d.setModal(False)
|
d.setModal(False)
|
||||||
|
@ -58,11 +58,15 @@ def get_mx(host, verbose=0):
|
|||||||
int(getattr(y, 'preference', sys.maxint))))
|
int(getattr(y, 'preference', sys.maxint))))
|
||||||
return [str(x.exchange) for x in answers if hasattr(x, 'exchange')]
|
return [str(x.exchange) for x in answers if hasattr(x, 'exchange')]
|
||||||
|
|
||||||
def sendmail_direct(from_, to, msg, timeout, localhost, verbose):
|
def sendmail_direct(from_, to, msg, timeout, localhost, verbose,
|
||||||
import smtplib
|
debug_output=None):
|
||||||
|
import calibre.utils.smtplib as smtplib
|
||||||
hosts = get_mx(to.split('@')[-1].strip(), verbose)
|
hosts = get_mx(to.split('@')[-1].strip(), verbose)
|
||||||
timeout=None # Non blocking sockets sometimes don't work
|
timeout=None # Non blocking sockets sometimes don't work
|
||||||
s = smtplib.SMTP(timeout=timeout, local_hostname=localhost)
|
kwargs = dict(timeout=timeout, local_hostname=localhost)
|
||||||
|
if debug_output is not None:
|
||||||
|
kwargs['debug_to'] = debug_output
|
||||||
|
s = smtplib.SMTP(**kwargs)
|
||||||
s.set_debuglevel(verbose)
|
s.set_debuglevel(verbose)
|
||||||
if not hosts:
|
if not hosts:
|
||||||
raise ValueError('No mail server found for address: %s'%to)
|
raise ValueError('No mail server found for address: %s'%to)
|
||||||
@ -79,17 +83,20 @@ def sendmail_direct(from_, to, msg, timeout, localhost, verbose):
|
|||||||
raise IOError('Failed to send mail: '+repr(last_error))
|
raise IOError('Failed to send mail: '+repr(last_error))
|
||||||
|
|
||||||
|
|
||||||
def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
|
def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=None,
|
||||||
relay=None, username=None, password=None, encryption='TLS',
|
relay=None, username=None, password=None, encryption='TLS',
|
||||||
port=-1):
|
port=-1, debug_output=None):
|
||||||
if relay is None:
|
if relay is None:
|
||||||
for x in to:
|
for x in to:
|
||||||
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
|
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
|
||||||
import smtplib
|
import calibre.utils.smtplib as smtplib
|
||||||
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
|
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
|
||||||
timeout = None # Non-blocking sockets sometimes don't work
|
timeout = None # Non-blocking sockets sometimes don't work
|
||||||
port = int(port)
|
port = int(port)
|
||||||
s = cls(timeout=timeout, local_hostname=localhost)
|
kwargs = dict(timeout=timeout, local_hostname=localhost)
|
||||||
|
if debug_output is not None:
|
||||||
|
kwargs['debug_to'] = debug_output
|
||||||
|
s = cls(**kwargs)
|
||||||
s.set_debuglevel(verbose)
|
s.set_debuglevel(verbose)
|
||||||
if port < 0:
|
if port < 0:
|
||||||
port = 25 if encryption == 'TLS' else 465
|
port = 25 if encryption == 'TLS' else 465
|
||||||
|
826
src/calibre/utils/smtplib.py
Executable file
826
src/calibre/utils/smtplib.py
Executable file
@ -0,0 +1,826 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
'''SMTP/ESMTP client class.
|
||||||
|
|
||||||
|
This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP
|
||||||
|
Authentication) and RFC 2487 (Secure SMTP over TLS).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
Please remember, when doing ESMTP, that the names of the SMTP service
|
||||||
|
extensions are NOT the same thing as the option keywords for the RCPT
|
||||||
|
and MAIL commands!
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> import smtplib
|
||||||
|
>>> s=smtplib.SMTP("localhost")
|
||||||
|
>>> print s.help()
|
||||||
|
This is Sendmail version 8.8.4
|
||||||
|
Topics:
|
||||||
|
HELO EHLO MAIL RCPT DATA
|
||||||
|
RSET NOOP QUIT HELP VRFY
|
||||||
|
EXPN VERB ETRN DSN
|
||||||
|
For more info use "HELP <topic>".
|
||||||
|
To report bugs in the implementation send email to
|
||||||
|
sendmail-bugs@sendmail.org.
|
||||||
|
For local information send email to Postmaster at your site.
|
||||||
|
End of HELP info
|
||||||
|
>>> s.putcmd("vrfy","someone@here")
|
||||||
|
>>> s.getreply()
|
||||||
|
(250, "Somebody OverHere <somebody@here.my.org>")
|
||||||
|
>>> s.quit()
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Author: The Dragon De Monsyne <dragondm@integral.org>
|
||||||
|
# ESMTP support, test code and doc fixes added by
|
||||||
|
# Eric S. Raymond <esr@thyrsus.com>
|
||||||
|
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
|
||||||
|
# by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
|
||||||
|
# RFC 2554 (authentication) support by Gerhard Haering <gerhard@bigfoot.de>.
|
||||||
|
# Enhanced debugging support by Kovid Goyal
|
||||||
|
#
|
||||||
|
# This was modified from the Python 1.5 library HTTP lib.
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import re
|
||||||
|
import email.utils
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import sys
|
||||||
|
from email.base64mime import encode as encode_base64
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
|
||||||
|
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
|
||||||
|
"SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
|
||||||
|
"quoteaddr","quotedata","SMTP"]
|
||||||
|
|
||||||
|
SMTP_PORT = 25
|
||||||
|
SMTP_SSL_PORT = 465
|
||||||
|
CRLF="\r\n"
|
||||||
|
|
||||||
|
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
||||||
|
|
||||||
|
# Exception classes used by this module.
|
||||||
|
class SMTPException(Exception):
|
||||||
|
"""Base class for all exceptions raised by this module."""
|
||||||
|
|
||||||
|
class SMTPServerDisconnected(SMTPException):
|
||||||
|
"""Not connected to any SMTP server.
|
||||||
|
|
||||||
|
This exception is raised when the server unexpectedly disconnects,
|
||||||
|
or when an attempt is made to use the SMTP instance before
|
||||||
|
connecting it to a server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SMTPResponseException(SMTPException):
|
||||||
|
"""Base class for all exceptions that include an SMTP error code.
|
||||||
|
|
||||||
|
These exceptions are generated in some instances when the SMTP
|
||||||
|
server returns an error code. The error code is stored in the
|
||||||
|
`smtp_code' attribute of the error, and the `smtp_error' attribute
|
||||||
|
is set to the error message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, code, msg):
|
||||||
|
self.smtp_code = code
|
||||||
|
self.smtp_error = msg
|
||||||
|
self.args = (code, msg)
|
||||||
|
|
||||||
|
class SMTPSenderRefused(SMTPResponseException):
|
||||||
|
"""Sender address refused.
|
||||||
|
|
||||||
|
In addition to the attributes set by on all SMTPResponseException
|
||||||
|
exceptions, this sets `sender' to the string that the SMTP refused.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, code, msg, sender):
|
||||||
|
self.smtp_code = code
|
||||||
|
self.smtp_error = msg
|
||||||
|
self.sender = sender
|
||||||
|
self.args = (code, msg, sender)
|
||||||
|
|
||||||
|
class SMTPRecipientsRefused(SMTPException):
|
||||||
|
"""All recipient addresses refused.
|
||||||
|
|
||||||
|
The errors for each recipient are accessible through the attribute
|
||||||
|
'recipients', which is a dictionary of exactly the same sort as
|
||||||
|
SMTP.sendmail() returns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, recipients):
|
||||||
|
self.recipients = recipients
|
||||||
|
self.args = ( recipients,)
|
||||||
|
|
||||||
|
|
||||||
|
class SMTPDataError(SMTPResponseException):
|
||||||
|
"""The SMTP server didn't accept the data."""
|
||||||
|
|
||||||
|
class SMTPConnectError(SMTPResponseException):
|
||||||
|
"""Error during connection establishment."""
|
||||||
|
|
||||||
|
class SMTPHeloError(SMTPResponseException):
|
||||||
|
"""The server refused our HELO reply."""
|
||||||
|
|
||||||
|
class SMTPAuthenticationError(SMTPResponseException):
|
||||||
|
"""Authentication error.
|
||||||
|
|
||||||
|
Most probably the server didn't accept the username/password
|
||||||
|
combination provided.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def quoteaddr(addr):
|
||||||
|
"""Quote a subset of the email addresses defined by RFC 821.
|
||||||
|
|
||||||
|
Should be able to handle anything rfc822.parseaddr can handle.
|
||||||
|
"""
|
||||||
|
m = (None, None)
|
||||||
|
try:
|
||||||
|
m = email.utils.parseaddr(addr)[1]
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if m == (None, None): # Indicates parse failure or AttributeError
|
||||||
|
# something weird here.. punt -ddm
|
||||||
|
return "<%s>" % addr
|
||||||
|
elif m is None:
|
||||||
|
# the sender wants an empty return address
|
||||||
|
return "<>"
|
||||||
|
else:
|
||||||
|
return "<%s>" % m
|
||||||
|
|
||||||
|
def quotedata(data):
|
||||||
|
"""Quote data for email.
|
||||||
|
|
||||||
|
Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
|
||||||
|
Internet CRLF end-of-line.
|
||||||
|
"""
|
||||||
|
return re.sub(r'(?m)^\.', '..',
|
||||||
|
re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
|
_have_ssl = False
|
||||||
|
else:
|
||||||
|
class SSLFakeFile:
|
||||||
|
"""A fake file like object that really wraps a SSLObject.
|
||||||
|
|
||||||
|
It only supports what is needed in smtplib.
|
||||||
|
"""
|
||||||
|
def __init__(self, sslobj):
|
||||||
|
self.sslobj = sslobj
|
||||||
|
|
||||||
|
def readline(self):
|
||||||
|
str = ""
|
||||||
|
chr = None
|
||||||
|
while chr != "\n":
|
||||||
|
chr = self.sslobj.read(1)
|
||||||
|
if not chr: break
|
||||||
|
str += chr
|
||||||
|
return str
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
_have_ssl = True
|
||||||
|
|
||||||
|
class SMTP:
|
||||||
|
"""This class manages a connection to an SMTP or ESMTP server.
|
||||||
|
SMTP Objects:
|
||||||
|
SMTP objects have the following attributes:
|
||||||
|
helo_resp
|
||||||
|
This is the message given by the server in response to the
|
||||||
|
most recent HELO command.
|
||||||
|
|
||||||
|
ehlo_resp
|
||||||
|
This is the message given by the server in response to the
|
||||||
|
most recent EHLO command. This is usually multiline.
|
||||||
|
|
||||||
|
does_esmtp
|
||||||
|
This is a True value _after you do an EHLO command_, if the
|
||||||
|
server supports ESMTP.
|
||||||
|
|
||||||
|
esmtp_features
|
||||||
|
This is a dictionary, which, if the server supports ESMTP,
|
||||||
|
will _after you do an EHLO command_, contain the names of the
|
||||||
|
SMTP service extensions this server supports, and their
|
||||||
|
parameters (if any).
|
||||||
|
|
||||||
|
Note, all extension names are mapped to lower case in the
|
||||||
|
dictionary.
|
||||||
|
|
||||||
|
See each method's docstrings for details. In general, there is a
|
||||||
|
method of the same name to perform each SMTP command. There is also a
|
||||||
|
method called 'sendmail' that will do an entire mail transaction.
|
||||||
|
"""
|
||||||
|
debuglevel = 0
|
||||||
|
file = None
|
||||||
|
helo_resp = None
|
||||||
|
ehlo_msg = "ehlo"
|
||||||
|
ehlo_resp = None
|
||||||
|
does_esmtp = 0
|
||||||
|
|
||||||
|
def __init__(self, host='', port=0, local_hostname=None,
|
||||||
|
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||||
|
debug_to=partial(print, file=sys.stderr)):
|
||||||
|
"""Initialize a new instance.
|
||||||
|
|
||||||
|
If specified, `host' is the name of the remote host to which to
|
||||||
|
connect. If specified, `port' specifies the port to which to connect.
|
||||||
|
By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
|
||||||
|
if the specified `host' doesn't respond correctly. If specified,
|
||||||
|
`local_hostname` is used as the FQDN of the local host. By default,
|
||||||
|
the local hostname is found using socket.getfqdn(). `debug_to`
|
||||||
|
specifies where debug output is written to. By default it is written to
|
||||||
|
sys.stderr. You should pass in a print function of your own to control
|
||||||
|
where debug output is written.
|
||||||
|
"""
|
||||||
|
self.timeout = timeout
|
||||||
|
self.debug = debug_to
|
||||||
|
self.esmtp_features = {}
|
||||||
|
self.default_port = SMTP_PORT
|
||||||
|
if host:
|
||||||
|
(code, msg) = self.connect(host, port)
|
||||||
|
if code != 220:
|
||||||
|
raise SMTPConnectError(code, msg)
|
||||||
|
if local_hostname is not None:
|
||||||
|
self.local_hostname = local_hostname
|
||||||
|
else:
|
||||||
|
# RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
|
||||||
|
# if that can't be calculated, that we should use a domain literal
|
||||||
|
# instead (essentially an encoded IP address like [A.B.C.D]).
|
||||||
|
fqdn = socket.getfqdn()
|
||||||
|
if '.' in fqdn:
|
||||||
|
self.local_hostname = fqdn
|
||||||
|
else:
|
||||||
|
# We can't find an fqdn hostname, so use a domain literal
|
||||||
|
addr = '127.0.0.1'
|
||||||
|
try:
|
||||||
|
addr = socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
pass
|
||||||
|
self.local_hostname = '[%s]' % addr
|
||||||
|
|
||||||
|
def set_debuglevel(self, debuglevel):
|
||||||
|
"""Set the debug output level.
|
||||||
|
|
||||||
|
A value of 0 means no debug logging. A value of 1 means all interaction
|
||||||
|
with the server is logged except that long lines are truncated to 100
|
||||||
|
characters and AUTH messages are censored. A value of 2 or higher means
|
||||||
|
the complete session is logged.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.debuglevel = debuglevel
|
||||||
|
|
||||||
|
def _get_socket(self, port, host, timeout):
|
||||||
|
# This makes it simpler for SMTP_SSL to use the SMTP connect code
|
||||||
|
# and just alter the socket connection bit.
|
||||||
|
if self.debuglevel > 0: self.debug('connect:', (host, port))
|
||||||
|
return socket.create_connection((port, host), timeout)
|
||||||
|
|
||||||
|
def connect(self, host='localhost', port = 0):
|
||||||
|
"""Connect to a host on a given port.
|
||||||
|
|
||||||
|
If the hostname ends with a colon (`:') followed by a number, and
|
||||||
|
there is no port specified, that suffix will be stripped off and the
|
||||||
|
number interpreted as the port number to use.
|
||||||
|
|
||||||
|
Note: This method is automatically invoked by __init__, if a host is
|
||||||
|
specified during instantiation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not port and (host.find(':') == host.rfind(':')):
|
||||||
|
i = host.rfind(':')
|
||||||
|
if i >= 0:
|
||||||
|
host, port = host[:i], host[i+1:]
|
||||||
|
try: port = int(port)
|
||||||
|
except ValueError:
|
||||||
|
raise socket.error, "nonnumeric port"
|
||||||
|
if not port: port = self.default_port
|
||||||
|
if self.debuglevel > 0: self.debug('connect:', (host, port))
|
||||||
|
self.sock = self._get_socket(host, port, self.timeout)
|
||||||
|
(code, msg) = self.getreply()
|
||||||
|
if self.debuglevel > 0: self.debug("connect:", msg)
|
||||||
|
return (code, msg)
|
||||||
|
|
||||||
|
def send(self, str):
|
||||||
|
"""Send `str' to the server."""
|
||||||
|
if self.debuglevel > 0:
|
||||||
|
raw = repr(str)
|
||||||
|
if self.debuglevel < 2:
|
||||||
|
if len(raw) > 100:
|
||||||
|
raw = raw[:100] + '...'
|
||||||
|
if 'AUTH' in raw:
|
||||||
|
raw = 'AUTH <censored>'
|
||||||
|
self.debug('send:', raw)
|
||||||
|
if hasattr(self, 'sock') and self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.sendall(str)
|
||||||
|
except socket.error:
|
||||||
|
self.close()
|
||||||
|
raise SMTPServerDisconnected('Server not connected')
|
||||||
|
else:
|
||||||
|
raise SMTPServerDisconnected('please run connect() first')
|
||||||
|
|
||||||
|
def putcmd(self, cmd, args=""):
|
||||||
|
"""Send a command to the server."""
|
||||||
|
if args == "":
|
||||||
|
str = '%s%s' % (cmd, CRLF)
|
||||||
|
else:
|
||||||
|
str = '%s %s%s' % (cmd, args, CRLF)
|
||||||
|
self.send(str)
|
||||||
|
|
||||||
|
def getreply(self):
|
||||||
|
"""Get a reply from the server.
|
||||||
|
|
||||||
|
Returns a tuple consisting of:
|
||||||
|
|
||||||
|
- server response code (e.g. '250', or such, if all goes well)
|
||||||
|
Note: returns -1 if it can't read response code.
|
||||||
|
|
||||||
|
- server response string corresponding to response code (multiline
|
||||||
|
responses are converted to a single, multiline string).
|
||||||
|
|
||||||
|
Raises SMTPServerDisconnected if end-of-file is reached.
|
||||||
|
"""
|
||||||
|
resp=[]
|
||||||
|
if self.file is None:
|
||||||
|
self.file = self.sock.makefile('rb')
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
line = self.file.readline()
|
||||||
|
except socket.error:
|
||||||
|
line = ''
|
||||||
|
if line == '':
|
||||||
|
self.close()
|
||||||
|
raise SMTPServerDisconnected("Connection unexpectedly closed")
|
||||||
|
if self.debuglevel > 0: self.debug('reply:', repr(line))
|
||||||
|
resp.append(line[4:].strip())
|
||||||
|
code=line[:3]
|
||||||
|
# Check that the error code is syntactically correct.
|
||||||
|
# Don't attempt to read a continuation line if it is broken.
|
||||||
|
try:
|
||||||
|
errcode = int(code)
|
||||||
|
except ValueError:
|
||||||
|
errcode = -1
|
||||||
|
break
|
||||||
|
# Check if multiline response.
|
||||||
|
if line[3:4]!="-":
|
||||||
|
break
|
||||||
|
|
||||||
|
errmsg = "\n".join(resp)
|
||||||
|
if self.debuglevel > 0:
|
||||||
|
self.debug('reply: retcode (%s); Msg: %s' % (errcode,errmsg))
|
||||||
|
return errcode, errmsg
|
||||||
|
|
||||||
|
def docmd(self, cmd, args=""):
|
||||||
|
"""Send a command, and return its response code."""
|
||||||
|
self.putcmd(cmd,args)
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
# std smtp commands
|
||||||
|
def helo(self, name=''):
|
||||||
|
"""SMTP 'helo' command.
|
||||||
|
Hostname to send for this command defaults to the FQDN of the local
|
||||||
|
host.
|
||||||
|
"""
|
||||||
|
self.putcmd("helo", name or self.local_hostname)
|
||||||
|
(code,msg)=self.getreply()
|
||||||
|
self.helo_resp=msg
|
||||||
|
return (code,msg)
|
||||||
|
|
||||||
|
def ehlo(self, name=''):
|
||||||
|
""" SMTP 'ehlo' command.
|
||||||
|
Hostname to send for this command defaults to the FQDN of the local
|
||||||
|
host.
|
||||||
|
"""
|
||||||
|
self.esmtp_features = {}
|
||||||
|
self.putcmd(self.ehlo_msg, name or self.local_hostname)
|
||||||
|
(code,msg)=self.getreply()
|
||||||
|
# According to RFC1869 some (badly written)
|
||||||
|
# MTA's will disconnect on an ehlo. Toss an exception if
|
||||||
|
# that happens -ddm
|
||||||
|
if code == -1 and len(msg) == 0:
|
||||||
|
self.close()
|
||||||
|
raise SMTPServerDisconnected("Server not connected")
|
||||||
|
self.ehlo_resp=msg
|
||||||
|
if code != 250:
|
||||||
|
return (code,msg)
|
||||||
|
self.does_esmtp=1
|
||||||
|
#parse the ehlo response -ddm
|
||||||
|
resp=self.ehlo_resp.split('\n')
|
||||||
|
del resp[0]
|
||||||
|
for each in resp:
|
||||||
|
# To be able to communicate with as many SMTP servers as possible,
|
||||||
|
# we have to take the old-style auth advertisement into account,
|
||||||
|
# because:
|
||||||
|
# 1) Else our SMTP feature parser gets confused.
|
||||||
|
# 2) There are some servers that only advertise the auth methods we
|
||||||
|
# support using the old style.
|
||||||
|
auth_match = OLDSTYLE_AUTH.match(each)
|
||||||
|
if auth_match:
|
||||||
|
# This doesn't remove duplicates, but that's no problem
|
||||||
|
self.esmtp_features["auth"] = self.esmtp_features.get("auth", "") \
|
||||||
|
+ " " + auth_match.groups(0)[0]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# RFC 1869 requires a space between ehlo keyword and parameters.
|
||||||
|
# It's actually stricter, in that only spaces are allowed between
|
||||||
|
# parameters, but were not going to check for that here. Note
|
||||||
|
# that the space isn't present if there are no parameters.
|
||||||
|
m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each)
|
||||||
|
if m:
|
||||||
|
feature=m.group("feature").lower()
|
||||||
|
params=m.string[m.end("feature"):].strip()
|
||||||
|
if feature == "auth":
|
||||||
|
self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \
|
||||||
|
+ " " + params
|
||||||
|
else:
|
||||||
|
self.esmtp_features[feature]=params
|
||||||
|
return (code,msg)
|
||||||
|
|
||||||
|
def has_extn(self, opt):
|
||||||
|
"""Does the server support a given SMTP service extension?"""
|
||||||
|
return opt.lower() in self.esmtp_features
|
||||||
|
|
||||||
|
def help(self, args=''):
|
||||||
|
"""SMTP 'help' command.
|
||||||
|
Returns help text from server."""
|
||||||
|
self.putcmd("help", args)
|
||||||
|
return self.getreply()[1]
|
||||||
|
|
||||||
|
def rset(self):
|
||||||
|
"""SMTP 'rset' command -- resets session."""
|
||||||
|
return self.docmd("rset")
|
||||||
|
|
||||||
|
def noop(self):
|
||||||
|
"""SMTP 'noop' command -- doesn't do anything :>"""
|
||||||
|
return self.docmd("noop")
|
||||||
|
|
||||||
|
def mail(self,sender,options=[]):
|
||||||
|
"""SMTP 'mail' command -- begins mail xfer session."""
|
||||||
|
optionlist = ''
|
||||||
|
if options and self.does_esmtp:
|
||||||
|
optionlist = ' ' + ' '.join(options)
|
||||||
|
self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
def rcpt(self,recip,options=[]):
|
||||||
|
"""SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
|
||||||
|
optionlist = ''
|
||||||
|
if options and self.does_esmtp:
|
||||||
|
optionlist = ' ' + ' '.join(options)
|
||||||
|
self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
def data(self,msg):
|
||||||
|
"""SMTP 'DATA' command -- sends message data to server.
|
||||||
|
|
||||||
|
Automatically quotes lines beginning with a period per rfc821.
|
||||||
|
Raises SMTPDataError if there is an unexpected reply to the
|
||||||
|
DATA command; the return value from this method is the final
|
||||||
|
response code received when the all data is sent.
|
||||||
|
"""
|
||||||
|
self.putcmd("data")
|
||||||
|
(code,repl)=self.getreply()
|
||||||
|
if self.debuglevel >0 : self.debug("data:", (code,repl))
|
||||||
|
if code != 354:
|
||||||
|
raise SMTPDataError(code,repl)
|
||||||
|
else:
|
||||||
|
q = quotedata(msg)
|
||||||
|
if q[-2:] != CRLF:
|
||||||
|
q = q + CRLF
|
||||||
|
q = q + "." + CRLF
|
||||||
|
self.send(q)
|
||||||
|
(code,msg)=self.getreply()
|
||||||
|
if self.debuglevel > 0 :
|
||||||
|
self.debug("data:", (code,msg))
|
||||||
|
return (code,msg)
|
||||||
|
|
||||||
|
def verify(self, address):
|
||||||
|
"""SMTP 'verify' command -- checks for address validity."""
|
||||||
|
self.putcmd("vrfy", quoteaddr(address))
|
||||||
|
return self.getreply()
|
||||||
|
# a.k.a.
|
||||||
|
vrfy=verify
|
||||||
|
|
||||||
|
def expn(self, address):
|
||||||
|
"""SMTP 'expn' command -- expands a mailing list."""
|
||||||
|
self.putcmd("expn", quoteaddr(address))
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
# some useful methods
|
||||||
|
|
||||||
|
def ehlo_or_helo_if_needed(self):
|
||||||
|
"""Call self.ehlo() and/or self.helo() if needed.
|
||||||
|
|
||||||
|
If there has been no previous EHLO or HELO command this session, this
|
||||||
|
method tries ESMTP EHLO first.
|
||||||
|
|
||||||
|
This method may raise the following exceptions:
|
||||||
|
|
||||||
|
SMTPHeloError The server didn't reply properly to
|
||||||
|
the helo greeting.
|
||||||
|
"""
|
||||||
|
if self.helo_resp is None and self.ehlo_resp is None:
|
||||||
|
if not (200 <= self.ehlo()[0] <= 299):
|
||||||
|
(code, resp) = self.helo()
|
||||||
|
if not (200 <= code <= 299):
|
||||||
|
raise SMTPHeloError(code, resp)
|
||||||
|
|
||||||
|
def login(self, user, password):
|
||||||
|
"""Log in on an SMTP server that requires authentication.
|
||||||
|
|
||||||
|
The arguments are:
|
||||||
|
- user: The user name to authenticate with.
|
||||||
|
- password: The password for the authentication.
|
||||||
|
|
||||||
|
If there has been no previous EHLO or HELO command this session, this
|
||||||
|
method tries ESMTP EHLO first.
|
||||||
|
|
||||||
|
This method will return normally if the authentication was successful.
|
||||||
|
|
||||||
|
This method may raise the following exceptions:
|
||||||
|
|
||||||
|
SMTPHeloError The server didn't reply properly to
|
||||||
|
the helo greeting.
|
||||||
|
SMTPAuthenticationError The server didn't accept the username/
|
||||||
|
password combination.
|
||||||
|
SMTPException No suitable authentication method was
|
||||||
|
found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def encode_cram_md5(challenge, user, password):
|
||||||
|
challenge = base64.decodestring(challenge)
|
||||||
|
response = user + " " + hmac.HMAC(password, challenge).hexdigest()
|
||||||
|
return encode_base64(response, eol="")
|
||||||
|
|
||||||
|
def encode_plain(user, password):
|
||||||
|
return encode_base64("\0%s\0%s" % (user, password), eol="")
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_PLAIN = "PLAIN"
|
||||||
|
AUTH_CRAM_MD5 = "CRAM-MD5"
|
||||||
|
AUTH_LOGIN = "LOGIN"
|
||||||
|
|
||||||
|
self.ehlo_or_helo_if_needed()
|
||||||
|
|
||||||
|
if not self.has_extn("auth"):
|
||||||
|
raise SMTPException("SMTP AUTH extension not supported by server.")
|
||||||
|
|
||||||
|
# Authentication methods the server supports:
|
||||||
|
authlist = self.esmtp_features["auth"].split()
|
||||||
|
|
||||||
|
# List of authentication methods we support: from preferred to
|
||||||
|
# less preferred methods. Except for the purpose of testing the weaker
|
||||||
|
# ones, we prefer stronger methods like CRAM-MD5:
|
||||||
|
preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN, AUTH_LOGIN]
|
||||||
|
|
||||||
|
# Determine the authentication method we'll use
|
||||||
|
authmethod = None
|
||||||
|
for method in preferred_auths:
|
||||||
|
if method in authlist:
|
||||||
|
authmethod = method
|
||||||
|
break
|
||||||
|
|
||||||
|
if authmethod == AUTH_CRAM_MD5:
|
||||||
|
(code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
|
||||||
|
if code == 503:
|
||||||
|
# 503 == 'Error: already authenticated'
|
||||||
|
return (code, resp)
|
||||||
|
(code, resp) = self.docmd(encode_cram_md5(resp, user, password))
|
||||||
|
elif authmethod == AUTH_PLAIN:
|
||||||
|
(code, resp) = self.docmd("AUTH",
|
||||||
|
AUTH_PLAIN + " " + encode_plain(user, password))
|
||||||
|
elif authmethod == AUTH_LOGIN:
|
||||||
|
(code, resp) = self.docmd("AUTH",
|
||||||
|
"%s %s" % (AUTH_LOGIN, encode_base64(user, eol="")))
|
||||||
|
if code != 334:
|
||||||
|
raise SMTPAuthenticationError(code, resp)
|
||||||
|
(code, resp) = self.docmd(encode_base64(password, eol=""))
|
||||||
|
elif authmethod is None:
|
||||||
|
raise SMTPException("No suitable authentication method found.")
|
||||||
|
if code not in (235, 503):
|
||||||
|
# 235 == 'Authentication successful'
|
||||||
|
# 503 == 'Error: already authenticated'
|
||||||
|
raise SMTPAuthenticationError(code, resp)
|
||||||
|
return (code, resp)
|
||||||
|
|
||||||
|
def starttls(self, keyfile = None, certfile = None):
|
||||||
|
"""Puts the connection to the SMTP server into TLS mode.
|
||||||
|
|
||||||
|
If there has been no previous EHLO or HELO command this session, this
|
||||||
|
method tries ESMTP EHLO first.
|
||||||
|
|
||||||
|
If the server supports TLS, this will encrypt the rest of the SMTP
|
||||||
|
session. If you provide the keyfile and certfile parameters,
|
||||||
|
the identity of the SMTP server and client can be checked. This,
|
||||||
|
however, depends on whether the socket module really checks the
|
||||||
|
certificates.
|
||||||
|
|
||||||
|
This method may raise the following exceptions:
|
||||||
|
|
||||||
|
SMTPHeloError The server didn't reply properly to
|
||||||
|
the helo greeting.
|
||||||
|
"""
|
||||||
|
self.ehlo_or_helo_if_needed()
|
||||||
|
if not self.has_extn("starttls"):
|
||||||
|
raise SMTPException("STARTTLS extension not supported by server.")
|
||||||
|
(resp, reply) = self.docmd("STARTTLS")
|
||||||
|
if resp == 220:
|
||||||
|
if not _have_ssl:
|
||||||
|
raise RuntimeError("No SSL support included in this Python")
|
||||||
|
self.sock = ssl.wrap_socket(self.sock, keyfile, certfile)
|
||||||
|
self.file = SSLFakeFile(self.sock)
|
||||||
|
# RFC 3207:
|
||||||
|
# The client MUST discard any knowledge obtained from
|
||||||
|
# the server, such as the list of SMTP service extensions,
|
||||||
|
# which was not obtained from the TLS negotiation itself.
|
||||||
|
self.helo_resp = None
|
||||||
|
self.ehlo_resp = None
|
||||||
|
self.esmtp_features = {}
|
||||||
|
self.does_esmtp = 0
|
||||||
|
return (resp, reply)
|
||||||
|
|
||||||
|
def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
|
||||||
|
rcpt_options=[]):
|
||||||
|
"""This command performs an entire mail transaction.
|
||||||
|
|
||||||
|
The arguments are:
|
||||||
|
- from_addr : The address sending this mail.
|
||||||
|
- to_addrs : A list of addresses to send this mail to. A bare
|
||||||
|
string will be treated as a list with 1 address.
|
||||||
|
- msg : The message to send.
|
||||||
|
- mail_options : List of ESMTP options (such as 8bitmime) for the
|
||||||
|
mail command.
|
||||||
|
- rcpt_options : List of ESMTP options (such as DSN commands) for
|
||||||
|
all the rcpt commands.
|
||||||
|
|
||||||
|
If there has been no previous EHLO or HELO command this session, this
|
||||||
|
method tries ESMTP EHLO first. If the server does ESMTP, message size
|
||||||
|
and each of the specified options will be passed to it. If EHLO
|
||||||
|
fails, HELO will be tried and ESMTP options suppressed.
|
||||||
|
|
||||||
|
This method will return normally if the mail is accepted for at least
|
||||||
|
one recipient. It returns a dictionary, with one entry for each
|
||||||
|
recipient that was refused. Each entry contains a tuple of the SMTP
|
||||||
|
error code and the accompanying error message sent by the server.
|
||||||
|
|
||||||
|
This method may raise the following exceptions:
|
||||||
|
|
||||||
|
SMTPHeloError The server didn't reply properly to
|
||||||
|
the helo greeting.
|
||||||
|
SMTPRecipientsRefused The server rejected ALL recipients
|
||||||
|
(no mail was sent).
|
||||||
|
SMTPSenderRefused The server didn't accept the from_addr.
|
||||||
|
SMTPDataError The server replied with an unexpected
|
||||||
|
error code (other than a refusal of
|
||||||
|
a recipient).
|
||||||
|
|
||||||
|
Note: the connection will be open even after an exception is raised.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> import smtplib
|
||||||
|
>>> s=smtplib.SMTP("localhost")
|
||||||
|
>>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
|
||||||
|
>>> msg = '''\\
|
||||||
|
... From: Me@my.org
|
||||||
|
... Subject: testin'...
|
||||||
|
...
|
||||||
|
... This is a test '''
|
||||||
|
>>> s.sendmail("me@my.org",tolist,msg)
|
||||||
|
{ "three@three.org" : ( 550 ,"User unknown" ) }
|
||||||
|
>>> s.quit()
|
||||||
|
|
||||||
|
In the above example, the message was accepted for delivery to three
|
||||||
|
of the four addresses, and one was rejected, with the error code
|
||||||
|
550. If all addresses are accepted, then the method will return an
|
||||||
|
empty dictionary.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.ehlo_or_helo_if_needed()
|
||||||
|
esmtp_opts = []
|
||||||
|
if self.does_esmtp:
|
||||||
|
# Hmmm? what's this? -ddm
|
||||||
|
# self.esmtp_features['7bit']=""
|
||||||
|
if self.has_extn('size'):
|
||||||
|
esmtp_opts.append("size=%d" % len(msg))
|
||||||
|
for option in mail_options:
|
||||||
|
esmtp_opts.append(option)
|
||||||
|
|
||||||
|
(code,resp) = self.mail(from_addr, esmtp_opts)
|
||||||
|
if code != 250:
|
||||||
|
self.rset()
|
||||||
|
raise SMTPSenderRefused(code, resp, from_addr)
|
||||||
|
senderrs={}
|
||||||
|
if isinstance(to_addrs, basestring):
|
||||||
|
to_addrs = [to_addrs]
|
||||||
|
for each in to_addrs:
|
||||||
|
(code,resp)=self.rcpt(each, rcpt_options)
|
||||||
|
if (code != 250) and (code != 251):
|
||||||
|
senderrs[each]=(code,resp)
|
||||||
|
if len(senderrs)==len(to_addrs):
|
||||||
|
# the server refused all our recipients
|
||||||
|
self.rset()
|
||||||
|
raise SMTPRecipientsRefused(senderrs)
|
||||||
|
(code,resp) = self.data(msg)
|
||||||
|
if code != 250:
|
||||||
|
self.rset()
|
||||||
|
raise SMTPDataError(code, resp)
|
||||||
|
#if we got here then somebody got our mail
|
||||||
|
return senderrs
|
||||||
|
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the connection to the SMTP server."""
|
||||||
|
if self.file:
|
||||||
|
self.file.close()
|
||||||
|
self.file = None
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
"""Terminate the SMTP session."""
|
||||||
|
res = self.docmd("quit")
|
||||||
|
self.close()
|
||||||
|
return res
|
||||||
|
|
||||||
|
if _have_ssl:
|
||||||
|
|
||||||
|
class SMTP_SSL(SMTP):
|
||||||
|
""" This is a subclass derived from SMTP that connects over an SSL encrypted
|
||||||
|
socket (to use this class you need a socket module that was compiled with SSL
|
||||||
|
support). If host is not specified, '' (the local host) is used. If port is
|
||||||
|
omitted, the standard SMTP-over-SSL port (465) is used. keyfile and certfile
|
||||||
|
are also optional - they can contain a PEM formatted private key and
|
||||||
|
certificate chain file for the SSL connection.
|
||||||
|
"""
|
||||||
|
def __init__(self, host='', port=0, local_hostname=None,
|
||||||
|
keyfile=None, certfile=None,
|
||||||
|
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||||
|
debug_to=partial(print, file=sys.stderr)):
|
||||||
|
self.keyfile = keyfile
|
||||||
|
self.certfile = certfile
|
||||||
|
SMTP.__init__(self, host, port, local_hostname, timeout,
|
||||||
|
debug_to=debug_to)
|
||||||
|
self.default_port = SMTP_SSL_PORT
|
||||||
|
|
||||||
|
def _get_socket(self, host, port, timeout):
|
||||||
|
if self.debuglevel > 0: self.debug('connect:', (host, port))
|
||||||
|
new_socket = socket.create_connection((host, port), timeout)
|
||||||
|
new_socket = ssl.wrap_socket(new_socket, self.keyfile, self.certfile)
|
||||||
|
self.file = SSLFakeFile(new_socket)
|
||||||
|
return new_socket
|
||||||
|
|
||||||
|
__all__.append("SMTP_SSL")
|
||||||
|
|
||||||
|
#
|
||||||
|
# LMTP extension
|
||||||
|
#
|
||||||
|
LMTP_PORT = 2003
|
||||||
|
|
||||||
|
class LMTP(SMTP):
|
||||||
|
"""LMTP - Local Mail Transfer Protocol
|
||||||
|
|
||||||
|
The LMTP protocol, which is very similar to ESMTP, is heavily based
|
||||||
|
on the standard SMTP client. It's common to use Unix sockets for LMTP,
|
||||||
|
so our connect() method must support that as well as a regular
|
||||||
|
host:port server. To specify a Unix socket, you must use an absolute
|
||||||
|
path as the host, starting with a '/'.
|
||||||
|
|
||||||
|
Authentication is supported, using the regular SMTP mechanism. When
|
||||||
|
using a Unix socket, LMTP generally don't support or require any
|
||||||
|
authentication, but your mileage might vary."""
|
||||||
|
|
||||||
|
ehlo_msg = "lhlo"
|
||||||
|
|
||||||
|
def __init__(self, host = '', port = LMTP_PORT, local_hostname = None):
|
||||||
|
"""Initialize a new instance."""
|
||||||
|
SMTP.__init__(self, host, port, local_hostname)
|
||||||
|
|
||||||
|
def connect(self, host = 'localhost', port = 0):
|
||||||
|
"""Connect to the LMTP daemon, on either a Unix or a TCP socket."""
|
||||||
|
if host[0] != '/':
|
||||||
|
return SMTP.connect(self, host, port)
|
||||||
|
|
||||||
|
# Handle Unix-domain sockets.
|
||||||
|
try:
|
||||||
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.sock.connect(host)
|
||||||
|
except socket.error, msg:
|
||||||
|
if self.debuglevel > 0: self.debug('connect fail:', host)
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
raise socket.error, msg
|
||||||
|
(code, msg) = self.getreply()
|
||||||
|
if self.debuglevel > 0: self.debug("connect:", msg)
|
||||||
|
return (code, msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user