tqt.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tqt.py (9900B)
---
1 #!/usr/bin/env python
2 #
3 # Electrum - Lightweight Bitcoin Client
4 # Copyright (C) 2015 Thomas Voegtlin
5 #
6 # Permission is hereby granted, free of charge, to any person
7 # obtaining a copy of this software and associated documentation files
8 # (the "Software"), to deal in the Software without restriction,
9 # including without limitation the rights to use, copy, modify, merge,
10 # publish, distribute, sublicense, and/or sell copies of the Software,
11 # and to permit persons to whom the Software is furnished to do so,
12 # subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be
15 # included in all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 # SOFTWARE.
25 import random
26 import time
27 import threading
28 import base64
29 from functools import partial
30 import traceback
31 import sys
32 from typing import Set
33
34 import smtplib
35 import imaplib
36 import email
37 from email.mime.multipart import MIMEMultipart
38 from email.mime.base import MIMEBase
39 from email.encoders import encode_base64
40
41 from PyQt5.QtCore import QObject, pyqtSignal, QThread
42 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit,
43 QInputDialog)
44
45 from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton,
46 WindowModalDialog)
47 from electrum.gui.qt.main_window import ElectrumWindow
48
49 from electrum.plugin import BasePlugin, hook
50 from electrum.paymentrequest import PaymentRequest
51 from electrum.i18n import _
52 from electrum.logging import Logger
53 from electrum.wallet import Abstract_Wallet
54 from electrum.invoices import OnchainInvoice
55
56
57 class Processor(threading.Thread, Logger):
58 polling_interval = 5*60
59
60 def __init__(self, imap_server, username, password, callback):
61 threading.Thread.__init__(self)
62 Logger.__init__(self)
63 self.daemon = True
64 self.username = username
65 self.password = password
66 self.imap_server = imap_server
67 self.on_receive = callback
68 self.M = None
69 self.reset_connect_wait()
70
71 def reset_connect_wait(self):
72 self.connect_wait = 100 # ms, between failed connection attempts
73
74 def poll(self):
75 try:
76 self.M.select()
77 except:
78 return
79 typ, data = self.M.search(None, 'ALL')
80 for num in str(data[0], 'utf8').split():
81 typ, msg_data = self.M.fetch(num, '(RFC822)')
82 msg = email.message_from_bytes(msg_data[0][1])
83 p = msg.get_payload()
84 if not msg.is_multipart():
85 p = [p]
86 continue
87 for item in p:
88 if item.get_content_type() == "application/bitcoin-paymentrequest":
89 pr_str = item.get_payload()
90 pr_str = base64.b64decode(pr_str)
91 self.on_receive(pr_str)
92
93 def run(self):
94 while True:
95 try:
96 self.M = imaplib.IMAP4_SSL(self.imap_server)
97 self.M.login(self.username, self.password)
98 except BaseException as e:
99 self.logger.info(f'connecting failed: {repr(e)}')
100 self.connect_wait *= 2
101 else:
102 self.reset_connect_wait()
103 # Reconnect when host changes
104 while self.M and self.M.host == self.imap_server:
105 try:
106 self.poll()
107 except BaseException as e:
108 self.logger.info(f'polling failed: {repr(e)}')
109 break
110 time.sleep(self.polling_interval)
111 time.sleep(random.randint(0, self.connect_wait))
112
113 def send(self, recipient, message, payment_request):
114 msg = MIMEMultipart()
115 msg['Subject'] = message
116 msg['To'] = recipient
117 msg['From'] = self.username
118 part = MIMEBase('application', "bitcoin-paymentrequest")
119 part.set_payload(payment_request)
120 encode_base64(part)
121 part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"')
122 msg.attach(part)
123 try:
124 s = smtplib.SMTP_SSL(self.imap_server, timeout=2)
125 s.login(self.username, self.password)
126 s.sendmail(self.username, [recipient], msg.as_string())
127 s.quit()
128 except BaseException as e:
129 self.logger.info(e)
130
131
132 class QEmailSignalObject(QObject):
133 email_new_invoice_signal = pyqtSignal()
134
135
136 class Plugin(BasePlugin):
137
138 def fullname(self):
139 return 'Email'
140
141 def description(self):
142 return _("Send and receive payment requests via email")
143
144 def is_available(self):
145 return True
146
147 def __init__(self, parent, config, name):
148 BasePlugin.__init__(self, parent, config, name)
149 self.imap_server = self.config.get('email_server', '')
150 self.username = self.config.get('email_username', '')
151 self.password = self.config.get('email_password', '')
152 if self.imap_server and self.username and self.password:
153 self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive)
154 self.processor.start()
155 self.obj = QEmailSignalObject()
156 self.obj.email_new_invoice_signal.connect(self.new_invoice)
157 self.wallets = set() # type: Set[Abstract_Wallet]
158
159 def on_receive(self, pr_str):
160 self.logger.info('received payment request')
161 self.pr = PaymentRequest(pr_str)
162 self.obj.email_new_invoice_signal.emit()
163
164 @hook
165 def load_wallet(self, wallet, main_window):
166 self.wallets |= {wallet}
167
168 @hook
169 def close_wallet(self, wallet):
170 self.wallets -= {wallet}
171
172 def new_invoice(self):
173 invoice = OnchainInvoice.from_bip70_payreq(self.pr)
174 for wallet in self.wallets:
175 wallet.save_invoice(invoice)
176 #main_window.invoice_list.update()
177
178 @hook
179 def receive_list_menu(self, window: ElectrumWindow, menu, addr):
180 menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr))
181
182 def send(self, window: ElectrumWindow, addr):
183 from electrum import paymentrequest
184 req = window.wallet.receive_requests.get(addr)
185 if not isinstance(req, OnchainInvoice):
186 window.show_error("Only on-chain requests are supported.")
187 return
188 message = req.message
189 if req.bip70:
190 payload = bytes.fromhex(req.bip70)
191 else:
192 pr = paymentrequest.make_request(self.config, req)
193 payload = pr.SerializeToString()
194 if not payload:
195 return
196 recipient, ok = QInputDialog.getText(window, 'Send request', 'Email invoice to:')
197 if not ok:
198 return
199 recipient = str(recipient)
200 self.logger.info(f'sending mail to {recipient}')
201 try:
202 # FIXME this runs in the GUI thread and blocks it...
203 self.processor.send(recipient, message, payload)
204 except BaseException as e:
205 self.logger.exception('')
206 window.show_message(repr(e))
207 else:
208 window.show_message(_('Request sent.'))
209
210 def requires_settings(self):
211 return True
212
213 def settings_widget(self, window):
214 return EnterButton(_('Settings'), partial(self.settings_dialog, window))
215
216 def settings_dialog(self, window):
217 d = WindowModalDialog(window, _("Email settings"))
218 d.setMinimumSize(500, 200)
219
220 vbox = QVBoxLayout(d)
221 vbox.addWidget(QLabel(_('Server hosting your email account')))
222 grid = QGridLayout()
223 vbox.addLayout(grid)
224 grid.addWidget(QLabel('Server (IMAP)'), 0, 0)
225 server_e = QLineEdit()
226 server_e.setText(self.imap_server)
227 grid.addWidget(server_e, 0, 1)
228
229 grid.addWidget(QLabel('Username'), 1, 0)
230 username_e = QLineEdit()
231 username_e.setText(self.username)
232 grid.addWidget(username_e, 1, 1)
233
234 grid.addWidget(QLabel('Password'), 2, 0)
235 password_e = QLineEdit()
236 password_e.setText(self.password)
237 grid.addWidget(password_e, 2, 1)
238
239 vbox.addStretch()
240 vbox.addLayout(Buttons(CloseButton(d), OkButton(d)))
241
242 if not d.exec_():
243 return
244
245 server = str(server_e.text())
246 self.config.set_key('email_server', server)
247 self.imap_server = server
248
249 username = str(username_e.text())
250 self.config.set_key('email_username', username)
251 self.username = username
252
253 password = str(password_e.text())
254 self.config.set_key('email_password', password)
255 self.password = password
256
257 check_connection = CheckConnectionThread(server, username, password)
258 check_connection.connection_error_signal.connect(lambda e: window.show_message(
259 _("Unable to connect to mail server:\n {}").format(e) + "\n" +
260 _("Please check your connection and credentials.")
261 ))
262 check_connection.start()
263
264
265 class CheckConnectionThread(QThread):
266 connection_error_signal = pyqtSignal(str)
267
268 def __init__(self, server, username, password):
269 super().__init__()
270 self.server = server
271 self.username = username
272 self.password = password
273
274 def run(self):
275 try:
276 conn = imaplib.IMAP4_SSL(self.server)
277 conn.login(self.username, self.password)
278 except BaseException as e:
279 self.connection_error_signal.emit(repr(e))