Allow sending by email to combinations of recipients

Sending by email: Allow sending by email to an arbitrary combination of
email address. Access it via the "Select recipients" menu entry in the
Email To menu. Fixes #1207818 [[Enhancement] - "Email to selected"](https://bugs.launchpad.net/calibre/+bug/1207818)
This commit is contained in:
Kovid Goyal 2013-08-08 16:01:19 +05:30
parent 560a097107
commit e5ce1ca2a4
4 changed files with 200 additions and 3 deletions

View File

@ -106,6 +106,15 @@ class ConvertAction(InterfaceAction):
self.book_auto_converted_mail, self.book_auto_converted_mail,
extra_job_args=[delete_from_library, to, fmts, subject]) extra_job_args=[delete_from_library, to, fmts, subject])
def auto_convert_multiple_mail(self, book_ids, data, ofmt, delete_from_library):
previous = self.gui.library_view.currentIndex()
rows = [x.row() for x in self.gui.library_view.selectionModel().selectedRows()]
jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, ofmt)
if jobs == []: return
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted_multiple_mail,
extra_job_args=[delete_from_library, data])
def auto_convert_news(self, book_ids, format): def auto_convert_news(self, book_ids, format):
previous = self.gui.library_view.currentIndex() previous = self.gui.library_view.currentIndex()
rows = [x.row() for x in \ rows = [x.row() for x in \
@ -207,6 +216,13 @@ class ConvertAction(InterfaceAction):
self.gui.send_by_mail(to, fmts, delete_from_library, subject=subject, self.gui.send_by_mail(to, fmts, delete_from_library, subject=subject,
specific_format=fmt, send_ids=[book_id], do_auto_convert=False) specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
def book_auto_converted_multiple_mail(self, job):
temp_files, fmt, book_id, delete_from_library, data = self.conversion_jobs[job]
self.book_converted(job)
for to, subject in data:
self.gui.send_by_mail(to, (fmt,), delete_from_library, subject=subject,
specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
def book_auto_converted_news(self, job): def book_auto_converted_news(self, job):
temp_files, fmt, book_id = self.conversion_jobs[job] temp_files, fmt, book_id = self.conversion_jobs[job]
self.book_converted(job) self.book_converted(job)

View File

@ -19,7 +19,7 @@ from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
from calibre.gui2 import info_dialog, question_dialog from calibre.gui2 import info_dialog, question_dialog
from calibre.library.server import server_config as content_server_config from calibre.library.server import server_config as content_server_config
class ShareConnMenu(QMenu): # {{{ class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal() connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal() connect_to_itunes = pyqtSignal()
@ -138,8 +138,17 @@ class ShareConnMenu(QMenu): # {{{
ac.a_s.connect(sync_menu.action_triggered) ac.a_s.connect(sync_menu.action_triggered)
action1.a_s.connect(sync_menu.action_triggered) action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered) action2.a_s.connect(sync_menu.action_triggered)
action1 = DeviceAction('choosemail:', False, False, I('mail.png'),
_('Select recipients'))
action2 = DeviceAction('choosemail:', True, False, I('mail.png'),
_('Select recipients') + ' ' + _('(delete from library)'))
self.email_to_menu.addAction(action1)
self.email_to_and_delete_menu.addAction(action2)
map(self.memory.append, (action1, action2))
ac = self.addMenu(self.email_to_and_delete_menu) ac = self.addMenu(self.email_to_and_delete_menu)
self.email_actions.append(ac) self.email_actions.append(ac)
action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered)
else: else:
ac = self.addAction(_('Setup email based sharing of books')) ac = self.addAction(_('Setup email based sharing of books'))
self.email_actions.append(ac) self.email_actions.append(ac)
@ -287,3 +296,4 @@ class ConnectShareAction(InterfaceAction):
ac.setIcon(QIcon(I('dot_%s.png'%icon))) ac.setIcon(QIcon(I('dot_%s.png'%icon)))
ac.setText(text) ac.setText(text)

View File

@ -1214,6 +1214,11 @@ class DeviceMixin(object): # {{{
subject = ';'.join(sub_dest_parts[2:]) subject = ';'.join(sub_dest_parts[2:])
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, subject=subject) self.send_by_mail(to, fmts, delete, subject=subject)
elif dest == 'choosemail':
from calibre.gui2.email import select_recipients
data = select_recipients(self)
if data:
self.send_multiple_by_mail(data, delete)
def cover_to_thumbnail(self, data): def cover_to_thumbnail(self, data):
if self.device_manager.device and \ if self.device_manager.device and \

View File

@ -11,6 +11,11 @@ from binascii import unhexlify
from functools import partial from functools import partial
from threading import Thread from threading import Thread
from itertools import repeat from itertools import repeat
from collections import defaultdict
from PyQt4.Qt import (
Qt, QDialog, QGridLayout, QIcon, QListWidget, QDialogButtonBox,
QListWidgetItem, QLabel, QLineEdit, QPushButton)
from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address, from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address,
config as email_config) config as email_config)
@ -18,9 +23,10 @@ from calibre.utils.filenames import ascii_filename
from calibre.customize.ui import available_input_formats, available_output_formats from calibre.customize.ui import available_input_formats, available_output_formats
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.gui2 import config, Dispatcher, warning_dialog from calibre.gui2 import config, Dispatcher, warning_dialog, error_dialog
from calibre.library.save_to_disk import get_components from calibre.library.save_to_disk import get_components
from calibre.utils.config import tweaks from calibre.utils.config import tweaks, prefs
from calibre.utils.icu import sort_key
from calibre.gui2.threaded_jobs import ThreadedJob from calibre.gui2.threaded_jobs import ThreadedJob
class Worker(Thread): class Worker(Thread):
@ -166,8 +172,164 @@ def email_news(mi, remove, get_fmts, done, job_manager):
plugboard_email_value = 'email' plugboard_email_value = 'email'
plugboard_email_formats = ['epub', 'mobi', 'azw3'] plugboard_email_formats = ['epub', 'mobi', 'azw3']
class SelectRecipients(QDialog): # {{{
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self._layout = l = QGridLayout(self)
self.setLayout(l)
self.setWindowIcon(QIcon(I('mail.png')))
self.setWindowTitle(_('Select recipients'))
self.recipients = r = QListWidget(self)
l.addWidget(r, 0, 0, 1, -1)
self.la = la = QLabel(_('Add a new recipient:'))
la.setStyleSheet('QLabel { font-weight: bold }')
l.addWidget(la, l.rowCount(), 0, 1, -1)
self.labels = tuple(map(QLabel, (
_('&Address'), _('A&lias'), _('&Formats'), _('&Subject'))))
tooltips = (
_('The email address of the recipient'),
_('The optional alias (simple name) of the recipient'),
_('Formats to email. The first matching one will be sent (comma separated list)'),
_('The optional subject for email sent to this recipient'))
for i, name in enumerate(('address', 'alias', 'formats', 'subject')):
c = i % 2
row = l.rowCount() - c
self.labels[i].setText(unicode(self.labels[i].text()) + ':')
l.addWidget(self.labels[i], row, (2*c))
le = QLineEdit(self)
le.setToolTip(tooltips[i])
setattr(self, name, le)
self.labels[i].setBuddy(le)
l.addWidget(le, row, (2*c) + 1)
self.formats.setText(prefs['output_format'].upper())
self.add_button = b = QPushButton(QIcon(I('plus.png')), _('&Add recipient'), self)
b.clicked.connect(self.add_recipient)
l.addWidget(b, l.rowCount(), 0, 1, -1)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
l.addWidget(bb, l.rowCount(), 0, 1, -1)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
self.setMinimumWidth(500)
self.setMinimumHeight(400)
self.resize(self.sizeHint())
self.init_list()
def add_recipient(self):
to = unicode(self.address.text()).strip()
if not to:
return error_dialog(
self, _('Need address'), _('You must specify an address'), show=True)
formats = ','.join([x.strip().upper() for x in unicode(self.formats.text()).strip().split(',') if x.strip()])
if not formats:
return error_dialog(
self, _('Need formats'), _('You must specify at least one format to send'), show=True)
opts = email_config().parse()
if to in opts.accounts:
return error_dialog(
self, _('Already exists'), _('The recipient %s already exists') % to, show=True)
acc = opts.accounts
acc[to] = [formats, False, False]
c = email_config()
c.set('accounts', acc)
alias = unicode(self.alias.text()).strip()
if alias:
opts.aliases[to] = alias
c.set('aliases', opts.aliases)
subject = unicode(self.subject.text()).strip()
if subject:
opts.subjects[to] = subject
c.set('subjects', opts.subjects)
self.create_item(alias or to, to, checked=True)
def create_item(self, alias, key, checked=False):
i = QListWidgetItem(alias, self.recipients)
i.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
i.setCheckState(Qt.Checked if checked else Qt.Unchecked)
i.setData(Qt.UserRole, key)
self.items.append(i)
def init_list(self):
opts = email_config().parse()
self.items = []
for key in sorted(opts.accounts or (), key=sort_key):
self.create_item(opts.aliases.get(key, key), key)
@property
def ans(self):
opts = email_config().parse()
ans = []
for i in self.items:
if i.checkState() == Qt.Checked:
to = unicode(i.data(Qt.UserRole).toString())
fmts = tuple(x.strip().upper() for x in (opts.accounts[to][0] or '').split(','))
subject = opts.subjects.get(to, '')
ans.append((to, fmts, subject))
return ans
def select_recipients(parent=None):
d = SelectRecipients(parent)
if d.exec_() == d.Accepted:
return d.ans
return ()
# }}}
class EmailMixin(object): # {{{ class EmailMixin(object): # {{{
def send_multiple_by_mail(self, recipients, delete_from_library):
ids = set(self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows())
if not ids:
return
db = self.current_db
db_fmt_map = {book_id:set((db.formats(book_id, index_is_id=True) or '').upper().split(',')) for book_id in ids}
ofmts = {x.upper() for x in available_output_formats()}
ifmts = {x.upper() for x in available_input_formats()}
bad_recipients = {}
auto_convert_map = defaultdict(list)
for to, fmts, subject in recipients:
rfmts = set(fmts)
ok_ids = {book_id for book_id, bfmts in db_fmt_map.iteritems() if bfmts.intersection(rfmts)}
convert_ids = ids - ok_ids
self.send_by_mail(to, fmts, delete_from_library, subject=subject, send_ids=ok_ids, do_auto_convert=False)
if not rfmts.intersection(ofmts):
bad_recipients[to] = (convert_ids, True)
continue
outfmt = tuple(f for f in fmts if f in ofmts)[0]
ok_ids = {book_id for book_id in convert_ids if db_fmt_map[book_id].intersection(ifmts)}
bad_ids = convert_ids - ok_ids
if bad_ids:
bad_recipients[to] = (bad_ids, False)
if ok_ids:
auto_convert_map[outfmt].append((to, subject, ok_ids))
if auto_convert_map:
titles = {book_id for x in auto_convert_map.itervalues() for data in x for book_id in data[2]}
titles = {db.title(book_id, index_is_id=True) for book_id in titles}
if self.auto_convert_question(
_('Auto convert the following books before sending via email?'), list(titles)):
for ofmt, data in auto_convert_map.iteritems():
ids = {bid for x in data for bid in x[2]}
data = [(to, subject) for to, subject, x in data]
self.iactions['Convert Books'].auto_convert_multiple_mail(ids, data, ofmt, delete_from_library)
if bad_recipients:
det_msg = []
titles = {book_id for x in bad_recipients.itervalues() for book_id in x[0]}
titles = {book_id:db.title(book_id, index_is_id=True) for book_id in titles}
for to, (ids, nooutput) in bad_recipients.iteritems():
msg = _('This recipient has no valid formats defined') if nooutput else \
_('These books have no suitable input formats for conversion')
det_msg.append('%s - %s' % (to, msg))
det_msg.extend('\t' + titles[bid] for bid in ids)
det_msg.append('\n')
warning_dialog(self, _('Could not send'),
_('Could not send books to some recipients. Click Show Details for more information'),
det_msg='\n'.join(det_msg), show=True)
def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None, def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None,
do_auto_convert=True, specific_format=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 ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
@ -307,4 +469,8 @@ class EmailMixin(object): # {{{
# }}} # }}}
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([]) # noqa
print (select_recipients())