1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
3# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>
4
5
6import re
7import textwrap
8
9from qt.core import QAbstractTableModel, QFont, Qt, QAbstractItemView
10
11from calibre.gui2 import gprefs
12from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
13from calibre.gui2.preferences.email_ui import Ui_Form
14from calibre.utils.config import ConfigProxy
15from calibre.utils.icu import numeric_sort_key
16from calibre.utils.smtp import config as smtp_prefs
17from polyglot.builtins import as_unicode
18
19
20class EmailAccounts(QAbstractTableModel):  # {{{
21
22    def __init__(self, accounts, subjects, aliases={}, tags={}):
23        QAbstractTableModel.__init__(self)
24        self.accounts = accounts
25        self.subjects = subjects
26        self.aliases = aliases
27        self.tags = tags
28        self.sorted_on = (0, True)
29        self.account_order = list(self.accounts)
30        self.do_sort()
31        self.headers  = [_('Email'), _('Formats'), _('Subject'),
32            _('Auto send'), _('Alias'), _('Auto send only tags')]
33        self.default_font = QFont()
34        self.default_font.setBold(True)
35        self.default_font = (self.default_font)
36        self.tooltips =[None] + list(map(textwrap.fill,
37            [_('Formats to email. The first matching format will be sent.'),
38             _('Subject of the email to use when sending. When left blank '
39               'the title will be used for the subject. Also, the same '
40               'templates used for "Save to disk" such as {title} and '
41               '{author_sort} can be used here.'),
42             '<p>'+_('If checked, downloaded news will be automatically '
43                     'mailed to this email address '
44                     '(provided it is in one of the listed formats and has not been filtered by tags).'),
45             _('Friendly name to use for this email address'),
46             _('If specified, only news with one of these tags will be sent to'
47               ' this email address. All news downloads have their title as a'
48               ' tag, so you can use this to easily control which news downloads'
49               ' are sent to this email address.')
50             ]))
51
52    def do_sort(self):
53        col = self.sorted_on[0]
54        if col == 0:
55            def key(account_key):
56                return numeric_sort_key(account_key)
57        elif col == 1:
58            def key(account_key):
59                return numeric_sort_key(self.accounts[account_key][0] or '')
60        elif col == 2:
61            def key(account_key):
62                return numeric_sort_key(self.subjects.get(account_key) or '')
63        elif col == 3:
64            def key(account_key):
65                return numeric_sort_key(as_unicode(self.accounts[account_key][0]) or '')
66        elif col == 4:
67            def key(account_key):
68                return numeric_sort_key(self.aliases.get(account_key) or '')
69        elif col == 5:
70            def key(account_key):
71                return numeric_sort_key(self.tags.get(account_key) or '')
72        self.account_order.sort(key=key, reverse=not self.sorted_on[1])
73
74    def sort(self, column, order=Qt.SortOrder.AscendingOrder):
75        nsort = (column, order == Qt.SortOrder.AscendingOrder)
76        if nsort != self.sorted_on:
77            self.sorted_on = nsort
78            self.beginResetModel()
79            try:
80                self.do_sort()
81            finally:
82                self.endResetModel()
83
84    def rowCount(self, *args):
85        return len(self.account_order)
86
87    def columnCount(self, *args):
88        return len(self.headers)
89
90    def headerData(self, section, orientation, role):
91        if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
92            return self.headers[section]
93        return None
94
95    def data(self, index, role):
96        row, col = index.row(), index.column()
97        if row < 0 or row >= self.rowCount():
98            return None
99        account = self.account_order[row]
100        if account not in self.accounts:
101            return None
102        if role == Qt.ItemDataRole.UserRole:
103            return (account, self.accounts[account])
104        if role == Qt.ItemDataRole.ToolTipRole:
105            return self.tooltips[col]
106        if role in [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole]:
107            if col == 0:
108                return (account)
109            if col ==  1:
110                return ', '.join(x.strip() for x in (self.accounts[account][0] or '').split(','))
111            if col == 2:
112                return (self.subjects.get(account, ''))
113            if col == 4:
114                return (self.aliases.get(account, ''))
115            if col == 5:
116                return (self.tags.get(account, ''))
117        if role == Qt.ItemDataRole.FontRole and self.accounts[account][2]:
118            return self.default_font
119        if role == Qt.ItemDataRole.CheckStateRole and col == 3:
120            return (Qt.CheckState.Checked if self.accounts[account][1] else Qt.CheckState.Unchecked)
121        return None
122
123    def flags(self, index):
124        if index.column() == 3:
125            return QAbstractTableModel.flags(self, index)|Qt.ItemFlag.ItemIsUserCheckable
126        else:
127            return QAbstractTableModel.flags(self, index)|Qt.ItemFlag.ItemIsEditable
128
129    def setData(self, index, value, role):
130        if not index.isValid():
131            return False
132        row, col = index.row(), index.column()
133        account = self.account_order[row]
134        if col == 3:
135            self.accounts[account][1] ^= True
136        elif col == 2:
137            self.subjects[account] = as_unicode(value or '')
138        elif col == 4:
139            self.aliases.pop(account, None)
140            aval = as_unicode(value or '').strip()
141            if aval:
142                self.aliases[account] = aval
143        elif col == 5:
144            self.tags.pop(account, None)
145            aval = as_unicode(value or '').strip()
146            if aval:
147                self.tags[account] = aval
148        elif col == 1:
149            self.accounts[account][0] = re.sub(',+', ',', re.sub(r'\s+', ',', as_unicode(value or '').upper()))
150        elif col == 0:
151            na = as_unicode(value or '')
152            from email.utils import parseaddr
153            addr = parseaddr(na)[-1]
154            if not addr:
155                return False
156            self.accounts[na] = self.accounts.pop(account)
157            self.account_order[row] = na
158            if '@kindle.com' in addr:
159                self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
160
161        self.dataChanged.emit(
162                self.index(index.row(), 0), self.index(index.row(), 3))
163        return True
164
165    def make_default(self, index):
166        if index.isValid():
167            self.beginResetModel()
168            row = index.row()
169            for x in self.accounts.values():
170                x[2] = False
171            self.accounts[self.account_order[row]][2] = True
172            self.endResetModel()
173
174    def add(self):
175        x = _('new email address')
176        y = x
177        c = 0
178        while y in self.accounts:
179            c += 1
180            y = x + str(c)
181        auto_send = len(self.accounts) < 1
182        self.beginResetModel()
183        self.accounts[y] = ['MOBI, EPUB', auto_send,
184                                                len(self.account_order) == 0]
185        self.account_order = list(self.accounts)
186        self.do_sort()
187        self.endResetModel()
188        return self.index(self.account_order.index(y), 0)
189
190    def remove(self, index):
191        if index.isValid():
192            row = index.row()
193            account = self.account_order[row]
194            self.accounts.pop(account)
195            self.account_order = sorted(self.accounts)
196            has_default = False
197            for account in self.account_order:
198                if self.accounts[account][2]:
199                    has_default = True
200                    break
201            if not has_default and self.account_order:
202                self.accounts[self.account_order[0]][2] = True
203
204            self.beginResetModel()
205            self.endResetModel()
206
207# }}}
208
209
210class ConfigWidget(ConfigWidgetBase, Ui_Form):
211
212    supports_restoring_to_defaults = False
213
214    def genesis(self, gui):
215        self.gui = gui
216        self.proxy = ConfigProxy(smtp_prefs())
217        r = self.register
218        r('add_comments_to_email', gprefs)
219
220        self.send_email_widget.initialize(self.preferred_to_address)
221        self.send_email_widget.changed_signal.connect(self.changed_signal.emit)
222        opts = self.send_email_widget.smtp_opts
223        self._email_accounts = EmailAccounts(opts.accounts, opts.subjects,
224                opts.aliases, opts.tags)
225        connect_lambda(self._email_accounts.dataChanged, self, lambda self: self.changed_signal.emit())
226        self.email_view.setModel(self._email_accounts)
227        self.email_view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
228        self.email_view.setSortingEnabled(True)
229
230        self.email_add.clicked.connect(self.add_email_account)
231        self.email_make_default.clicked.connect(self.make_default)
232        self.email_view.resizeColumnsToContents()
233        self.email_remove.clicked.connect(self.remove_email_account)
234
235    def preferred_to_address(self):
236        if self._email_accounts.account_order:
237            return self._email_accounts.account_order[0]
238
239    def initialize(self):
240        ConfigWidgetBase.initialize(self)
241        # Initializing all done in genesis
242
243    def restore_defaults(self):
244        ConfigWidgetBase.restore_defaults(self)
245        # No defaults to restore to
246
247    def commit(self):
248        if self.email_view.state() == QAbstractItemView.State.EditingState:
249            # Ensure that the cell being edited is committed by switching focus
250            # to some other widget, which automatically closes the open editor
251            self.send_email_widget.setFocus(Qt.FocusReason.OtherFocusReason)
252        to_set = bool(self._email_accounts.accounts)
253        if not self.send_email_widget.set_email_settings(to_set):
254            raise AbortCommit('abort')
255        self.proxy['accounts'] =  self._email_accounts.accounts
256        self.proxy['subjects'] = self._email_accounts.subjects
257        self.proxy['aliases'] = self._email_accounts.aliases
258        self.proxy['tags'] = self._email_accounts.tags
259
260        return ConfigWidgetBase.commit(self)
261
262    def make_default(self, *args):
263        self._email_accounts.make_default(self.email_view.currentIndex())
264        self.changed_signal.emit()
265
266    def add_email_account(self, *args):
267        index = self._email_accounts.add()
268        self.email_view.setCurrentIndex(index)
269        self.email_view.resizeColumnsToContents()
270        self.email_view.edit(index)
271        self.changed_signal.emit()
272
273    def remove_email_account(self, *args):
274        idx = self.email_view.currentIndex()
275        self._email_accounts.remove(idx)
276        self.changed_signal.emit()
277
278    def refresh_gui(self, gui):
279        from calibre.gui2.email import gui_sendmail
280        gui_sendmail.calculate_rate_limit()
281
282
283if __name__ == '__main__':
284    from calibre.gui2 import Application
285    app = Application([])
286    test_widget('Sharing', 'Email')
287