electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

qt.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))