From e5ce1ca2a469156ebb3697903ba7fade25324cf2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 8 Aug 2013 16:01:19 +0530 Subject: [PATCH] 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) --- src/calibre/gui2/actions/convert.py | 16 +++ src/calibre/gui2/actions/device.py | 12 +- src/calibre/gui2/device.py | 5 + src/calibre/gui2/email.py | 170 +++++++++++++++++++++++++++- 4 files changed, 200 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index c5e1580c6d..aaadd1a452 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -106,6 +106,15 @@ class ConvertAction(InterfaceAction): self.book_auto_converted_mail, 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): previous = self.gui.library_view.currentIndex() 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, 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): temp_files, fmt, book_id = self.conversion_jobs[job] self.book_converted(job) diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 09cadceb9c..e6432f9334 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -19,7 +19,7 @@ from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog from calibre.gui2 import info_dialog, question_dialog from calibre.library.server import server_config as content_server_config -class ShareConnMenu(QMenu): # {{{ +class ShareConnMenu(QMenu): # {{{ connect_to_folder = pyqtSignal() connect_to_itunes = pyqtSignal() @@ -138,8 +138,17 @@ class ShareConnMenu(QMenu): # {{{ ac.a_s.connect(sync_menu.action_triggered) action1.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) self.email_actions.append(ac) + action1.a_s.connect(sync_menu.action_triggered) + action2.a_s.connect(sync_menu.action_triggered) else: ac = self.addAction(_('Setup email based sharing of books')) self.email_actions.append(ac) @@ -287,3 +296,4 @@ class ConnectShareAction(InterfaceAction): ac.setIcon(QIcon(I('dot_%s.png'%icon))) ac.setText(text) + diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index d3225e66e7..bddfa3efa9 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1214,6 +1214,11 @@ class DeviceMixin(object): # {{{ subject = ';'.join(sub_dest_parts[2:]) fmts = [x.strip().lower() for x in fmts.split(',')] 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): if self.device_manager.device and \ diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index 6645441158..5ecce8248f 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -11,6 +11,11 @@ from binascii import unhexlify from functools import partial from threading import Thread 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, 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.ebooks.metadata import authors_to_string 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.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 class Worker(Thread): @@ -166,8 +172,164 @@ def email_news(mi, remove, get_fmts, done, job_manager): plugboard_email_value = 'email' 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): # {{{ + 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, 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 @@ -307,4 +469,8 @@ class EmailMixin(object): # {{{ # }}} +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) # noqa + print (select_recipients())