import datetime import pytz import requests import smtplib import ssl from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from pathlib import Path from dateutil import parser as date_parser from jinja2 import Environment, FileSystemLoader from __main__ import config from __main__ import email_log as log class Email(): def __init__(self, address): self.address = address log.debug(f'{self.address}: Creating email') self.content = {} self.from_address = config['email']['address'] self.from_name = config['email']['from'] log.debug(( f'{self.address}: Sending from {self.from_address} ' + f'({self.from_name})')) def pretty_time(self, expiry): current_time = datetime.datetime.now() date = expiry.strftime(config['email']['date_format']) if config.getboolean('email', 'use_24h'): log.debug(f'{self.address}: Using 24h time') time = expiry.strftime('%H:%M') else: log.debug(f'{self.address}: Using 12h time') time = expiry.strftime('%-I:%M %p') expiry_delta = (expiry - current_time).seconds expires_in = {'hours': expiry_delta//3600, 'minutes': (expiry_delta//60) % 60} if expires_in['hours'] == 0: expires_in = f'{str(expires_in["minutes"])}m' else: expires_in = (f'{str(expires_in["hours"])}h ' + f'{str(expires_in["minutes"])}m') log.debug(f'{self.address}: Expires in {expires_in}') return {'date': date, 'time': time, 'expires_in': expires_in} def construct_invite(self, invite): self.subject = config['invite_emails']['subject'] log.debug(f'{self.address}: Using subject {self.subject}') log.debug(f'{self.address}: Constructing email content') expiry = invite['expiry'] expiry.replace(tzinfo=None) pretty = self.pretty_time(expiry) email_message = config['email']['message'] invite_link = config['invite_emails']['url_base'] invite_link += '/' + invite['code'] for key in ['text', 'html']: sp = Path(config['invite_emails']['email_' + key]) / '..' sp = str(sp.resolve()) + '/' template_loader = FileSystemLoader(searchpath=sp) template_env = Environment(loader=template_loader) fname = Path(config['invite_emails']['email_' + key]).name template = template_env.get_template(fname) c = template.render(expiry_date=pretty['date'], expiry_time=pretty['time'], expires_in=pretty['expires_in'], invite_link=invite_link, message=email_message) self.content[key] = c log.info(f'{self.address}: {key} constructed') def construct_reset(self, reset): self.subject = config['password_resets']['subject'] log.debug(f'{self.address}: Using subject {self.subject}') log.debug(f'{self.address}: Constructing email content') try: expiry = date_parser.parse(reset['ExpirationDate']) expiry = expiry.replace(tzinfo=None) except: log.error(f"{self.address}: Couldn't parse expiry time") return False current_time = datetime.datetime.now() if expiry >= current_time: log.debug(f'{self.address}: Invite valid') pretty = self.pretty_time(expiry) email_message = config['email']['message'] for key in ['text', 'html']: sp = Path(config['password_resets']['email_' + key]) / '..' sp = str(sp.resolve()) + '/' template_loader = FileSystemLoader(searchpath=sp) template_env = Environment(loader=template_loader) fname = Path(config['password_resets']['email_' + key]).name template = template_env.get_template(fname) c = template.render(username=reset['UserName'], expiry_date=pretty['date'], expiry_time=pretty['time'], expires_in=pretty['expires_in'], pin=reset['Pin'], message=email_message) self.content[key] = c log.info(f'{self.address}: {key} constructed') return True else: err = ((f"{self.address}: " + "Reset has reportedly already expired. " + "Ensure timezones are correctly configured.")) log.error(err) return False class Mailgun(Email): def __init__(self, address): super().__init__(address) self.api_url = config['mailgun']['api_url'] self.api_key = config['mailgun']['api_key'] self.from_mg = f'{self.from_name} <{self.from_address}>' def send(self): response = requests.post(self.api_url, auth=("api", self.api_key), data={"from": self.from_mg, "to": [self.address], "subject": self.subject, "text": self.content['text'], "html": self.content['html']}) if response.ok: log.info(f'{self.address}: Sent via mailgun.') return True log.debug(f'{self.address}: Mailgun: {response.status_code}') return response class Smtp(Email): def __init__(self, address): super().__init__(address) self.server = config['smtp']['server'] self.password = config['smtp']['password'] try: self.port = int(config['smtp']['port']) except ValueError: self.port = 465 log.debug(f'{self.address}: Defaulting to port {self.port}') def send(self): message = MIMEMultipart("alternative") message["Subject"] = self.subject message["From"] = self.from_address message["To"] = self.address text = MIMEText(self.content['text'], 'plain') html = MIMEText(self.content['html'], 'html') message.attach(text) message.attach(html) try: if config['smtp']['encryption'] == 'ssl_tls': self.context = ssl.create_default_context() with smtplib.SMTP_SSL(self.server, self.port, context=self.context) as server: server.ehlo() server.login(self.from_address, self.password) server.sendmail(self.from_address, self.address, message.as_string()) log.info(f'{self.address}: Sent via smtp (ssl/tls)') return True elif config['smtp']['encryption'] == 'starttls': with smtplib.SMTP(self.server, self.port) as server: server.ehlo() server.starttls() server.login(self.from_address, self.password) server.sendmail(self.from_address, self.address, message.as_string()) log.info(f'{self.address}: Sent via smtp (starttls)') return True except Exception as e: err = f'{self.address}: Failed to send via smtp: ' err += type(e).__name__ log.error(err) try: log.error(e.smtp_error) except: pass