URI: 
       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))