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