ThomasV
3 years ago
2 changed files with 0 additions and 287 deletions
@ -1,5 +0,0 @@ |
|||
from electrum.i18n import _ |
|||
|
|||
fullname = _('Email') |
|||
description = _("Send and receive payment request with an email account") |
|||
available_for = ['qt'] |
@ -1,282 +0,0 @@ |
|||
#!/usr/bin/env python |
|||
# |
|||
# Electrum - Lightweight Bitcoin Client |
|||
# Copyright (C) 2015 Thomas Voegtlin |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person |
|||
# obtaining a copy of this software and associated documentation files |
|||
# (the "Software"), to deal in the Software without restriction, |
|||
# including without limitation the rights to use, copy, modify, merge, |
|||
# publish, distribute, sublicense, and/or sell copies of the Software, |
|||
# and to permit persons to whom the Software is furnished to do so, |
|||
# subject to the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
import random |
|||
import time |
|||
import threading |
|||
import base64 |
|||
from functools import partial |
|||
import traceback |
|||
import sys |
|||
from typing import Set |
|||
import ssl |
|||
import smtplib |
|||
import imaplib |
|||
import email |
|||
from email.mime.multipart import MIMEMultipart |
|||
from email.mime.base import MIMEBase |
|||
from email.encoders import encode_base64 |
|||
|
|||
from PyQt5.QtCore import QObject, pyqtSignal, QThread |
|||
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit, |
|||
QInputDialog) |
|||
import certifi |
|||
|
|||
from electrum.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, |
|||
WindowModalDialog) |
|||
from electrum.gui.qt.main_window import ElectrumWindow |
|||
|
|||
from electrum.plugin import BasePlugin, hook |
|||
from electrum.paymentrequest import PaymentRequest |
|||
from electrum.i18n import _ |
|||
from electrum.logging import Logger |
|||
from electrum.wallet import Abstract_Wallet |
|||
from electrum.invoices import Invoice |
|||
|
|||
|
|||
ca_path = certifi.where() |
|||
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path) |
|||
|
|||
|
|||
class Processor(threading.Thread, Logger): |
|||
polling_interval = 5*60 |
|||
|
|||
def __init__(self, imap_server, username, password, callback): |
|||
threading.Thread.__init__(self) |
|||
Logger.__init__(self) |
|||
self.daemon = True |
|||
self.username = username |
|||
self.password = password |
|||
self.imap_server = imap_server |
|||
self.on_receive = callback |
|||
self.M = None |
|||
self.reset_connect_wait() |
|||
|
|||
def reset_connect_wait(self): |
|||
self.connect_wait = 100 # ms, between failed connection attempts |
|||
|
|||
def poll(self): |
|||
try: |
|||
self.M.select() |
|||
except: |
|||
return |
|||
typ, data = self.M.search(None, 'ALL') |
|||
for num in str(data[0], 'utf8').split(): |
|||
typ, msg_data = self.M.fetch(num, '(RFC822)') |
|||
msg = email.message_from_bytes(msg_data[0][1]) |
|||
p = msg.get_payload() |
|||
if not msg.is_multipart(): |
|||
p = [p] |
|||
continue |
|||
for item in p: |
|||
if item.get_content_type() == "application/bitcoin-paymentrequest": |
|||
pr_str = item.get_payload() |
|||
pr_str = base64.b64decode(pr_str) |
|||
self.on_receive(pr_str) |
|||
|
|||
def run(self): |
|||
while True: |
|||
try: |
|||
self.M = imaplib.IMAP4_SSL(self.imap_server, ssl_context=ssl_context) |
|||
self.M.login(self.username, self.password) |
|||
except BaseException as e: |
|||
self.logger.info(f'connecting failed: {repr(e)}') |
|||
self.connect_wait *= 2 |
|||
else: |
|||
self.reset_connect_wait() |
|||
# Reconnect when host changes |
|||
while self.M and self.M.host == self.imap_server: |
|||
try: |
|||
self.poll() |
|||
except BaseException as e: |
|||
self.logger.info(f'polling failed: {repr(e)}') |
|||
break |
|||
time.sleep(self.polling_interval) |
|||
time.sleep(random.randint(0, self.connect_wait)) |
|||
|
|||
def send(self, recipient, message, payment_request): |
|||
msg = MIMEMultipart() |
|||
msg['Subject'] = message |
|||
msg['To'] = recipient |
|||
msg['From'] = self.username |
|||
part = MIMEBase('application', "bitcoin-paymentrequest") |
|||
part.set_payload(payment_request) |
|||
encode_base64(part) |
|||
part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"') |
|||
msg.attach(part) |
|||
try: |
|||
s = smtplib.SMTP_SSL(self.imap_server, timeout=2, context=ssl_context) |
|||
s.login(self.username, self.password) |
|||
s.sendmail(self.username, [recipient], msg.as_string()) |
|||
s.quit() |
|||
except BaseException as e: |
|||
self.logger.info(e) |
|||
|
|||
|
|||
class QEmailSignalObject(QObject): |
|||
email_new_invoice_signal = pyqtSignal() |
|||
|
|||
|
|||
class Plugin(BasePlugin): |
|||
|
|||
def fullname(self): |
|||
return 'Email' |
|||
|
|||
def description(self): |
|||
return _("Send and receive payment requests via email") |
|||
|
|||
def is_available(self): |
|||
return True |
|||
|
|||
def __init__(self, parent, config, name): |
|||
BasePlugin.__init__(self, parent, config, name) |
|||
self.imap_server = self.config.get('email_server', '') |
|||
self.username = self.config.get('email_username', '') |
|||
self.password = self.config.get('email_password', '') |
|||
if self.imap_server and self.username and self.password: |
|||
self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive) |
|||
self.processor.start() |
|||
self.obj = QEmailSignalObject() |
|||
self.obj.email_new_invoice_signal.connect(self.new_invoice) |
|||
self.wallets = set() # type: Set[Abstract_Wallet] |
|||
|
|||
def on_receive(self, pr_str): |
|||
self.logger.info('received payment request') |
|||
self.pr = PaymentRequest(pr_str) |
|||
self.obj.email_new_invoice_signal.emit() |
|||
|
|||
@hook |
|||
def load_wallet(self, wallet, main_window): |
|||
self.wallets |= {wallet} |
|||
|
|||
@hook |
|||
def close_wallet(self, wallet): |
|||
self.wallets -= {wallet} |
|||
|
|||
def new_invoice(self): |
|||
invoice = Invoice.from_bip70_payreq(self.pr) |
|||
for wallet in self.wallets: |
|||
wallet.save_invoice(invoice) |
|||
#main_window.invoice_list.update() |
|||
|
|||
@hook |
|||
def receive_list_menu(self, window: ElectrumWindow, menu, addr): |
|||
menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) |
|||
|
|||
def send(self, window: ElectrumWindow, addr): |
|||
from electrum import paymentrequest |
|||
req = window.wallet.get_request(addr) |
|||
# FIXME only on-chain requests are supported |
|||
message = req.message |
|||
if req.bip70: |
|||
payload = bytes.fromhex(req.bip70) |
|||
else: |
|||
pr = paymentrequest.make_request(self.config, req) |
|||
payload = pr.SerializeToString() |
|||
if not payload: |
|||
return |
|||
recipient, ok = QInputDialog.getText(window, 'Send request', 'Email invoice to:') |
|||
if not ok: |
|||
return |
|||
recipient = str(recipient) |
|||
self.logger.info(f'sending mail to {recipient}') |
|||
try: |
|||
# FIXME this runs in the GUI thread and blocks it... |
|||
self.processor.send(recipient, message, payload) |
|||
except BaseException as e: |
|||
self.logger.exception('') |
|||
window.show_message(repr(e)) |
|||
else: |
|||
window.show_message(_('Request sent.')) |
|||
|
|||
def requires_settings(self): |
|||
return True |
|||
|
|||
def settings_widget(self, window): |
|||
return EnterButton(_('Settings'), partial(self.settings_dialog, window)) |
|||
|
|||
def settings_dialog(self, window): |
|||
d = WindowModalDialog(window, _("Email settings")) |
|||
d.setMinimumSize(500, 200) |
|||
|
|||
vbox = QVBoxLayout(d) |
|||
vbox.addWidget(QLabel(_('Server hosting your email account'))) |
|||
grid = QGridLayout() |
|||
vbox.addLayout(grid) |
|||
grid.addWidget(QLabel('Server (IMAP)'), 0, 0) |
|||
server_e = QLineEdit() |
|||
server_e.setText(self.imap_server) |
|||
grid.addWidget(server_e, 0, 1) |
|||
|
|||
grid.addWidget(QLabel('Username'), 1, 0) |
|||
username_e = QLineEdit() |
|||
username_e.setText(self.username) |
|||
grid.addWidget(username_e, 1, 1) |
|||
|
|||
grid.addWidget(QLabel('Password'), 2, 0) |
|||
password_e = QLineEdit() |
|||
password_e.setText(self.password) |
|||
grid.addWidget(password_e, 2, 1) |
|||
|
|||
vbox.addStretch() |
|||
vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) |
|||
|
|||
if not d.exec_(): |
|||
return |
|||
|
|||
server = str(server_e.text()) |
|||
self.config.set_key('email_server', server) |
|||
self.imap_server = server |
|||
|
|||
username = str(username_e.text()) |
|||
self.config.set_key('email_username', username) |
|||
self.username = username |
|||
|
|||
password = str(password_e.text()) |
|||
self.config.set_key('email_password', password) |
|||
self.password = password |
|||
|
|||
check_connection = CheckConnectionThread(server, username, password) |
|||
check_connection.connection_error_signal.connect(lambda e: window.show_message( |
|||
_("Unable to connect to mail server:\n {}").format(e) + "\n" + |
|||
_("Please check your connection and credentials.") |
|||
)) |
|||
check_connection.start() |
|||
|
|||
|
|||
class CheckConnectionThread(QThread): |
|||
connection_error_signal = pyqtSignal(str) |
|||
|
|||
def __init__(self, server, username, password): |
|||
super().__init__() |
|||
self.server = server |
|||
self.username = username |
|||
self.password = password |
|||
|
|||
def run(self): |
|||
try: |
|||
conn = imaplib.IMAP4_SSL(self.server, ssl_context=ssl_context) |
|||
conn.login(self.username, self.password) |
|||
except BaseException as e: |
|||
self.connection_error_signal.emit(repr(e)) |
Loading…
Reference in new issue