1# -*- coding: utf-8 -*- 2# 3# This file is part of urlwatch (https://thp.io/2008/urlwatch/). 4# Copyright (c) 2008-2021 Thomas Perl <m@thp.io> 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 11# 1. Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# 2. Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in the 15# documentation and/or other materials provided with the distribution. 16# 3. The name of the author may not be used to endorse or promote products 17# derived from this software without specific prior written permission. 18# 19# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 20# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 24# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 31import smtplib 32import getpass 33import subprocess 34import logging 35 36try: 37 import keyring 38except ImportError: 39 keyring = None 40 41import email.mime.multipart 42import email.mime.text 43import email.utils 44 45logger = logging.getLogger(__name__) 46 47 48class Mailer(object): 49 def send(self, msg): 50 raise NotImplementedError 51 52 def msg_plain(self, from_email, to_email, subject, body): 53 msg = email.mime.text.MIMEText(body, 'plain', 'utf-8') 54 msg['Subject'] = subject 55 msg['From'] = from_email 56 msg['To'] = to_email 57 msg['Date'] = email.utils.formatdate() 58 59 return msg 60 61 def msg_html(self, from_email, to_email, subject, body_text, body_html): 62 msg = email.mime.multipart.MIMEMultipart('alternative') 63 msg['Subject'] = subject 64 msg['From'] = from_email 65 msg['To'] = to_email 66 msg['Date'] = email.utils.formatdate() 67 68 msg.attach(email.mime.text.MIMEText(body_text, 'plain', 'utf-8')) 69 msg.attach(email.mime.text.MIMEText(body_html, 'html', 'utf-8')) 70 71 return msg 72 73 74class SMTPMailer(Mailer): 75 def __init__(self, smtp_user, smtp_server, smtp_port, tls, auth, insecure_password=None): 76 self.smtp_server = smtp_server 77 self.smtp_user = smtp_user 78 self.smtp_port = smtp_port 79 self.tls = tls 80 self.auth = auth 81 self.insecure_password = insecure_password 82 83 def send(self, msg): 84 s = smtplib.SMTP(self.smtp_server, self.smtp_port) 85 s.ehlo() 86 87 if self.tls: 88 s.starttls() 89 90 if self.auth: 91 if self.insecure_password: 92 passwd = self.insecure_password 93 elif keyring is not None: 94 passwd = keyring.get_password(self.smtp_server, self.smtp_user) 95 if passwd is None: 96 raise ValueError('No password available in keyring for {}, {}' 97 .format(self.smtp_server, self.smtp_user)) 98 else: 99 raise ValueError('SMTP auth is enabled, but insecure_password is not set and keyring is not available') 100 s.login(self.smtp_user, passwd) 101 102 s.sendmail(msg['From'], msg['To'].split(','), msg.as_string()) 103 s.quit() 104 105 106class SendmailMailer(Mailer): 107 def __init__(self, sendmail_path): 108 self.sendmail_path = sendmail_path 109 110 def send(self, msg): 111 p = subprocess.Popen([self.sendmail_path, '-oi', '-f', msg['From'], msg['To']], 112 stdin=subprocess.PIPE, 113 stderr=subprocess.PIPE, 114 universal_newlines=True) 115 result = p.communicate(msg.as_string()) 116 if p.returncode: 117 logger.error('Sendmail failed with {result}'.format(result=result)) 118 119 120def have_password(smtp_server, from_email): 121 return keyring.get_password(smtp_server, from_email) is not None 122 123 124def set_password(smtp_server, from_email): 125 ''' Set the keyring password for the mail connection. Interactive.''' 126 if keyring is None: 127 raise ImportError('keyring module missing - service unsupported') 128 129 password = getpass.getpass(prompt='Enter password for {} using {}: '.format(from_email, smtp_server)) 130 keyring.set_password(smtp_server, from_email, password) 131