Split the code into smaller modules
Introduce modules: - lacre.transport - for actual delivery via SMTP - lacre.smime - to take care of S/MIME stuff Implement lacre.transport.SendFrom class that does a almost exactly the same thing as the original send_msg function, but without using global variable to store original message sender.
This commit is contained in:
parent
ff6e0bfbdd
commit
682de14630
|
@ -64,6 +64,6 @@ if not delivered:
|
||||||
# silly message-encoding issue that shouldn't bounce the message, we just
|
# silly message-encoding issue that shouldn't bounce the message, we just
|
||||||
# try recoding the message body and delivering it.
|
# try recoding the message body and delivering it.
|
||||||
try:
|
try:
|
||||||
core.failover_delivery(raw_message, to_addrs)
|
core.failover_delivery(raw_message, to_addrs, from_addr)
|
||||||
except:
|
except:
|
||||||
LOG.exception('Failover delivery failed too')
|
LOG.exception('Failover delivery failed too')
|
||||||
|
|
133
lacre/core.py
133
lacre/core.py
|
@ -30,19 +30,16 @@ from email.message import EmailMessage, MIMEPart
|
||||||
import email.utils
|
import email.utils
|
||||||
from email.policy import SMTPUTF8
|
from email.policy import SMTPUTF8
|
||||||
import GnuPG
|
import GnuPG
|
||||||
import os
|
|
||||||
import smtplib
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Tuple, List, AnyStr
|
from typing import Tuple
|
||||||
|
|
||||||
# imports for S/MIME
|
|
||||||
from M2Crypto import BIO, SMIME, X509
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import lacre.text as text
|
import lacre.text as text
|
||||||
import lacre.config as conf
|
import lacre.config as conf
|
||||||
import lacre.keyring as kcache
|
import lacre.keyring as kcache
|
||||||
import lacre.recipients as recpt
|
import lacre.recipients as recpt
|
||||||
|
import lacre.smime as smime
|
||||||
|
from lacre.transport import send_msg, register_sender, SendFrom
|
||||||
from lacre.mailop import KeepIntact, InlineOpenPGPEncrypt, MimeOpenPGPEncrypt
|
from lacre.mailop import KeepIntact, InlineOpenPGPEncrypt, MimeOpenPGPEncrypt
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,92 +286,6 @@ def _append_gpg_extension(attachment):
|
||||||
attachment.set_param('name', pgpFilename)
|
attachment.set_param('name', pgpFilename)
|
||||||
|
|
||||||
|
|
||||||
def _smime_encrypt(raw_message, recipients):
|
|
||||||
global LOG
|
|
||||||
global from_addr
|
|
||||||
|
|
||||||
if not conf.config_item_set('smime', 'cert_path'):
|
|
||||||
LOG.info("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
|
|
||||||
return recipients
|
|
||||||
|
|
||||||
cert_path = conf.get_item('smime', 'cert_path')+"/"
|
|
||||||
s = SMIME.SMIME()
|
|
||||||
sk = X509.X509_Stack()
|
|
||||||
smime_to = list()
|
|
||||||
unsmime_to = list()
|
|
||||||
|
|
||||||
for addr in recipients:
|
|
||||||
cert_and_email = _get_cert_for_email(addr, cert_path)
|
|
||||||
|
|
||||||
if not (cert_and_email is None):
|
|
||||||
(to_cert, normal_email) = cert_and_email
|
|
||||||
LOG.debug("Found cert " + to_cert + " for " + addr + ": " + normal_email)
|
|
||||||
smime_to.append(addr)
|
|
||||||
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
|
|
||||||
sk.push(x509)
|
|
||||||
else:
|
|
||||||
unsmime_to.append(addr)
|
|
||||||
|
|
||||||
if smime_to:
|
|
||||||
s.set_x509_stack(sk)
|
|
||||||
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
|
|
||||||
p7 = s.encrypt(BIO.MemoryBuffer(raw_message.as_string()))
|
|
||||||
# Output p7 in mail-friendly format.
|
|
||||||
out = BIO.MemoryBuffer()
|
|
||||||
out.write('From: ' + from_addr + text.EOL_S)
|
|
||||||
out.write('To: ' + raw_message['To'] + text.EOL_S)
|
|
||||||
if raw_message['Cc']:
|
|
||||||
out.write('Cc: ' + raw_message['Cc'] + text.EOL_S)
|
|
||||||
if raw_message['Bcc']:
|
|
||||||
out.write('Bcc: ' + raw_message['Bcc'] + text.EOL_S)
|
|
||||||
if raw_message['Subject']:
|
|
||||||
out.write('Subject: ' + raw_message['Subject'] + text.EOL_S)
|
|
||||||
|
|
||||||
if conf.config_item_equals('default', 'add_header', 'yes'):
|
|
||||||
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate' + text.EOL_S)
|
|
||||||
|
|
||||||
s.write(out, p7)
|
|
||||||
|
|
||||||
LOG.debug(f"Sending message from {from_addr} to {smime_to}")
|
|
||||||
|
|
||||||
send_msg(out.read(), smime_to)
|
|
||||||
if unsmime_to:
|
|
||||||
LOG.debug(f"Unable to find valid S/MIME certificates for {unsmime_to}")
|
|
||||||
|
|
||||||
return unsmime_to
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cert_for_email(to_addr, cert_path):
|
|
||||||
insensitive = conf.config_item_equals('default', 'mail_case_insensitive', 'yes')
|
|
||||||
|
|
||||||
LOG.info(f'Retrieving certificate for {to_addr!r} from {cert_path!r}, sensitivity={insensitive!r}')
|
|
||||||
|
|
||||||
files_in_directory = os.listdir(cert_path)
|
|
||||||
for filename in files_in_directory:
|
|
||||||
file_path = os.path.join(cert_path, filename)
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if insensitive:
|
|
||||||
if filename.casefold() == to_addr:
|
|
||||||
return (file_path, to_addr)
|
|
||||||
else:
|
|
||||||
if filename == to_addr:
|
|
||||||
return (file_path, to_addr)
|
|
||||||
|
|
||||||
# support foo+ignore@bar.com -> foo@bar.com
|
|
||||||
LOG.info(f"An email with topic? {to_addr}")
|
|
||||||
(fixed_up_email, topic) = text.parse_delimiter(to_addr)
|
|
||||||
LOG.info(f'Got {fixed_up_email!r} and {topic!r}')
|
|
||||||
if topic is None:
|
|
||||||
# delimiter not used
|
|
||||||
LOG.info('Topic not found')
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
LOG.info(f"Looking up certificate for {fixed_up_email} after parsing {to_addr}")
|
|
||||||
return _get_cert_for_email(fixed_up_email, cert_path)
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_message_from_payloads(payloads, message=None):
|
def _generate_message_from_payloads(payloads, message=None):
|
||||||
if message is None:
|
if message is None:
|
||||||
message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype())
|
message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype())
|
||||||
|
@ -395,46 +306,28 @@ def _get_first_payload(payloads):
|
||||||
return payloads
|
return payloads
|
||||||
|
|
||||||
|
|
||||||
def send_msg(message: AnyStr, recipients: List[str], fromaddr=None):
|
|
||||||
"""Send MESSAGE to RECIPIENTS to the mail relay."""
|
|
||||||
global from_addr
|
|
||||||
|
|
||||||
if fromaddr is not None:
|
|
||||||
from_addr = fromaddr
|
|
||||||
|
|
||||||
recipients = [_f for _f in recipients if _f]
|
|
||||||
if recipients:
|
|
||||||
LOG.info(f"Sending email to: {recipients!r}")
|
|
||||||
relay = conf.relay_params()
|
|
||||||
smtp = smtplib.SMTP(relay[0], relay[1])
|
|
||||||
if conf.flag_enabled('relay', 'starttls'):
|
|
||||||
smtp.starttls()
|
|
||||||
smtp.sendmail(from_addr, recipients, message)
|
|
||||||
else:
|
|
||||||
LOG.info("No recipient found")
|
|
||||||
|
|
||||||
|
|
||||||
def _recode(m: EmailMessage):
|
def _recode(m: EmailMessage):
|
||||||
payload = m.get_payload()
|
payload = m.get_payload()
|
||||||
m.set_content(payload)
|
m.set_content(payload)
|
||||||
|
|
||||||
|
|
||||||
def failover_delivery(message: EmailMessage, recipients):
|
def failover_delivery(message: EmailMessage, recipients, from_address):
|
||||||
"""Try delivering message just one last time."""
|
"""Try delivering message just one last time."""
|
||||||
LOG.debug('Failover delivery')
|
LOG.debug('Failover delivery')
|
||||||
|
|
||||||
|
send = SendFrom(from_address)
|
||||||
if message.get_content_maintype() == 'text':
|
if message.get_content_maintype() == 'text':
|
||||||
LOG.debug('Flat text message, adjusting coding')
|
LOG.debug('Flat text message, adjusting coding')
|
||||||
_recode(message)
|
_recode(message)
|
||||||
b = message.as_bytes(policy=SMTPUTF8)
|
b = message.as_bytes(policy=SMTPUTF8)
|
||||||
send_msg(b, recipients)
|
send(b, recipients)
|
||||||
elif message.get_content_maintype() == 'multipart':
|
elif message.get_content_maintype() == 'multipart':
|
||||||
LOG.debug('Multipart message, adjusting coding of text entities')
|
LOG.debug('Multipart message, adjusting coding of text entities')
|
||||||
for part in message.iter_parts():
|
for part in message.iter_parts():
|
||||||
if part.get_content_maintype() == 'text':
|
if part.get_content_maintype() == 'text':
|
||||||
_recode(part)
|
_recode(part)
|
||||||
b = message.as_bytes(policy=SMTPUTF8)
|
b = message.as_bytes(policy=SMTPUTF8)
|
||||||
send_msg(b, recipients)
|
send(b, recipients)
|
||||||
else:
|
else:
|
||||||
LOG.warning('No failover strategy, giving up')
|
LOG.warning('No failover strategy, giving up')
|
||||||
|
|
||||||
|
@ -475,19 +368,19 @@ def delivery_plan(recipients, message: EmailMessage, key_cache: kcache.KeyCache)
|
||||||
|
|
||||||
def deliver_message(raw_message: EmailMessage, from_address, to_addrs):
|
def deliver_message(raw_message: EmailMessage, from_address, to_addrs):
|
||||||
"""Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available."""
|
"""Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available."""
|
||||||
global from_addr
|
|
||||||
|
|
||||||
# Ugly workaround to keep the code working without too many changes.
|
# Ugly workaround to keep the code working without too many changes.
|
||||||
from_addr = from_address
|
register_sender(from_address)
|
||||||
|
|
||||||
sanitize = text.choose_sanitizer(conf.get_item('default', 'mail_case_insensitive'))
|
sanitize = text.choose_sanitizer(conf.get_item('default', 'mail_case_insensitive'))
|
||||||
recipients_left = [sanitize(recipient) for recipient in to_addrs]
|
recipients_left = [sanitize(recipient) for recipient in to_addrs]
|
||||||
|
|
||||||
|
send = SendFrom(from_address)
|
||||||
|
|
||||||
# There is no need for nested encryption
|
# There is no need for nested encryption
|
||||||
LOG.debug("Seeing if it's already encrypted")
|
LOG.debug("Seeing if it's already encrypted")
|
||||||
if _is_encrypted(raw_message):
|
if _is_encrypted(raw_message):
|
||||||
LOG.debug("Message is already encrypted. Encryption aborted.")
|
LOG.debug("Message is already encrypted. Encryption aborted.")
|
||||||
send_msg(raw_message.as_string(), recipients_left)
|
send(raw_message.as_string(), recipients_left)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Encrypt mails for recipients with known public PGP keys
|
# Encrypt mails for recipients with known public PGP keys
|
||||||
|
@ -498,10 +391,10 @@ def deliver_message(raw_message: EmailMessage, from_address, to_addrs):
|
||||||
|
|
||||||
# Encrypt mails for recipients with known S/MIME certificate
|
# Encrypt mails for recipients with known S/MIME certificate
|
||||||
LOG.debug("Encrypting with S/MIME")
|
LOG.debug("Encrypting with S/MIME")
|
||||||
recipients_left = _smime_encrypt(raw_message, recipients_left)
|
recipients_left = smime.encrypt(raw_message, recipients_left, from_address)
|
||||||
if not recipients_left:
|
if not recipients_left:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Send out mail to recipients which are left
|
# Send out mail to recipients which are left
|
||||||
LOG.debug("Sending the rest as text/plain")
|
LOG.debug("Sending the rest as text/plain")
|
||||||
send_msg(raw_message.as_string(), recipients_left)
|
send(raw_message.as_string(), recipients_left)
|
||||||
|
|
|
@ -13,12 +13,6 @@ from email.policy import SMTPUTF8
|
||||||
import time
|
import time
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
# Mail status constants.
|
|
||||||
#
|
|
||||||
# These are the only values that our mail handler is allowed to return.
|
|
||||||
RESULT_OK = '250 OK'
|
|
||||||
RESULT_ERROR = '500 Could not process your message'
|
|
||||||
|
|
||||||
# Load configuration and init logging, in this order. Only then can we load
|
# Load configuration and init logging, in this order. Only then can we load
|
||||||
# the last Lacre module, i.e. lacre.mailgate.
|
# the last Lacre module, i.e. lacre.mailgate.
|
||||||
conf.load_config()
|
conf.load_config()
|
||||||
|
@ -28,6 +22,7 @@ LOG = logging.getLogger('lacre.daemon')
|
||||||
from GnuPG import EncryptionException
|
from GnuPG import EncryptionException
|
||||||
import lacre.core as gate
|
import lacre.core as gate
|
||||||
import lacre.keyring as kcache
|
import lacre.keyring as kcache
|
||||||
|
import lacre.transport as xport
|
||||||
from lacre.mailop import KeepIntact
|
from lacre.mailop import KeepIntact
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,32 +52,33 @@ class MailEncryptionProxy:
|
||||||
if conf.flag_enabled('daemon', 'log_headers'):
|
if conf.flag_enabled('daemon', 'log_headers'):
|
||||||
LOG.info('Message headers: %s', self._extract_headers(message))
|
LOG.info('Message headers: %s', self._extract_headers(message))
|
||||||
|
|
||||||
|
send = xport.SendFrom(envelope.mail_from)
|
||||||
for operation in gate.delivery_plan(envelope.rcpt_tos, message, keys):
|
for operation in gate.delivery_plan(envelope.rcpt_tos, message, keys):
|
||||||
LOG.debug(f"Sending mail via {operation!r}")
|
LOG.debug(f"Sending mail via {operation!r}")
|
||||||
try:
|
try:
|
||||||
new_message = operation.perform(message)
|
new_message = operation.perform(message)
|
||||||
gate.send_msg(new_message, operation.recipients(), envelope.mail_from)
|
send(new_message, operation.recipients())
|
||||||
except EncryptionException:
|
except EncryptionException:
|
||||||
# If the message can't be encrypted, deliver cleartext.
|
# If the message can't be encrypted, deliver cleartext.
|
||||||
LOG.exception('Unable to encrypt message, delivering in cleartext')
|
LOG.exception('Unable to encrypt message, delivering in cleartext')
|
||||||
if not isinstance(operation, KeepIntact):
|
if not isinstance(operation, KeepIntact):
|
||||||
self._send_unencrypted(operation, message, envelope)
|
self._send_unencrypted(operation, message, envelope, send)
|
||||||
else:
|
else:
|
||||||
LOG.error(f'Cannot perform {operation}')
|
LOG.error(f'Cannot perform {operation}')
|
||||||
|
|
||||||
except:
|
except:
|
||||||
LOG.exception('Unexpected exception caught, bouncing message')
|
LOG.exception('Unexpected exception caught, bouncing message')
|
||||||
return RESULT_ERROR
|
return xport.RESULT_ERROR
|
||||||
|
|
||||||
ellapsed = (time.process_time() - start) * 1000
|
ellapsed = (time.process_time() - start) * 1000
|
||||||
LOG.info(f'Message delivered in {ellapsed:.2f} ms')
|
LOG.info(f'Message delivered in {ellapsed:.2f} ms')
|
||||||
|
|
||||||
return RESULT_OK
|
return xport.RESULT_OK
|
||||||
|
|
||||||
def _send_unencrypted(self, operation, message, envelope):
|
def _send_unencrypted(self, operation, message, envelope, send: xport.SendFrom):
|
||||||
keep = KeepIntact(operation.recipients())
|
keep = KeepIntact(operation.recipients())
|
||||||
new_message = keep.perform(message)
|
new_message = keep.perform(message)
|
||||||
gate.send_msg(new_message, operation.recipients(), envelope.mail_from)
|
send(new_message, operation.recipients(), envelope.mail_from)
|
||||||
|
|
||||||
def _beginning(self, e: Envelope) -> bytes:
|
def _beginning(self, e: Envelope) -> bytes:
|
||||||
double_eol_pos = e.original_content.find(DOUBLE_EOL_BYTES)
|
double_eol_pos = e.original_content.find(DOUBLE_EOL_BYTES)
|
||||||
|
|
|
@ -32,7 +32,23 @@ import lacre.text as text
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GpgRecipient:
|
class Recipient:
|
||||||
|
"""Wraps recipient's email."""
|
||||||
|
|
||||||
|
def __init__(self, email):
|
||||||
|
"""Initialise the recipient."""
|
||||||
|
self._email = email
|
||||||
|
|
||||||
|
def email(self) -> str:
|
||||||
|
"""Return email address of this recipient."""
|
||||||
|
return self._email
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return string representation of this recipient: the email address."""
|
||||||
|
return self._email
|
||||||
|
|
||||||
|
|
||||||
|
class GpgRecipient(Recipient):
|
||||||
"""A tuple-like object that contains GPG recipient data."""
|
"""A tuple-like object that contains GPG recipient data."""
|
||||||
|
|
||||||
def __init__(self, left, right):
|
def __init__(self, left, right):
|
||||||
|
@ -53,7 +69,7 @@ class GpgRecipient:
|
||||||
"""Return textual representation of this GPG Recipient."""
|
"""Return textual representation of this GPG Recipient."""
|
||||||
return f"GpgRecipient({self._left!r}, {self._right!r})"
|
return f"GpgRecipient({self._left!r}, {self._right!r})"
|
||||||
|
|
||||||
def email(self):
|
def email(self) -> str:
|
||||||
"""Return this recipient's email address."""
|
"""Return this recipient's email address."""
|
||||||
return self._left
|
return self._left
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
#
|
||||||
|
# gpg-mailgate
|
||||||
|
#
|
||||||
|
# This file is part of the gpg-mailgate source code.
|
||||||
|
#
|
||||||
|
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""S/MIME handling module."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from M2Crypto import BIO, SMIME, X509
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import lacre.text as text
|
||||||
|
import lacre.config as conf
|
||||||
|
import lacre.transport as xport
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# WARNING: This file is not covered with E2E tests.
|
||||||
|
#
|
||||||
|
|
||||||
|
def encrypt(raw_message, recipients, from_addr):
|
||||||
|
"""Encrypt with S/MIME."""
|
||||||
|
if not conf.config_item_set('smime', 'cert_path'):
|
||||||
|
LOG.info("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
|
||||||
|
return recipients
|
||||||
|
|
||||||
|
cert_path = conf.get_item('smime', 'cert_path')+"/"
|
||||||
|
s = SMIME.SMIME()
|
||||||
|
sk = X509.X509_Stack()
|
||||||
|
smime_to = list()
|
||||||
|
cleartext_to = list()
|
||||||
|
|
||||||
|
for addr in recipients:
|
||||||
|
cert_and_email = _get_cert_for_email(addr, cert_path)
|
||||||
|
|
||||||
|
if not (cert_and_email is None):
|
||||||
|
(to_cert, normal_email) = cert_and_email
|
||||||
|
LOG.debug("Found cert " + to_cert + " for " + addr + ": " + normal_email)
|
||||||
|
smime_to.append(addr)
|
||||||
|
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
|
||||||
|
sk.push(x509)
|
||||||
|
else:
|
||||||
|
cleartext_to.append(addr)
|
||||||
|
|
||||||
|
if smime_to:
|
||||||
|
s.set_x509_stack(sk)
|
||||||
|
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
|
||||||
|
p7 = s.encrypt(BIO.MemoryBuffer(raw_message.as_string()))
|
||||||
|
# Output p7 in mail-friendly format.
|
||||||
|
out = BIO.MemoryBuffer()
|
||||||
|
out.write('From: ' + from_addr + text.EOL_S)
|
||||||
|
out.write('To: ' + raw_message['To'] + text.EOL_S)
|
||||||
|
if raw_message['Cc']:
|
||||||
|
out.write('Cc: ' + raw_message['Cc'] + text.EOL_S)
|
||||||
|
if raw_message['Bcc']:
|
||||||
|
out.write('Bcc: ' + raw_message['Bcc'] + text.EOL_S)
|
||||||
|
if raw_message['Subject']:
|
||||||
|
out.write('Subject: ' + raw_message['Subject'] + text.EOL_S)
|
||||||
|
|
||||||
|
if conf.config_item_equals('default', 'add_header', 'yes'):
|
||||||
|
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate' + text.EOL_S)
|
||||||
|
|
||||||
|
s.write(out, p7)
|
||||||
|
|
||||||
|
LOG.debug(f"Sending message from {from_addr} to {smime_to}")
|
||||||
|
|
||||||
|
send_msg = xport.SendFrom(from_addr)
|
||||||
|
send_msg(out.read(), smime_to)
|
||||||
|
|
||||||
|
if cleartext_to:
|
||||||
|
LOG.debug(f"Unable to find valid S/MIME certificates for {cleartext_to}")
|
||||||
|
|
||||||
|
return cleartext_to
|
||||||
|
|
||||||
|
|
||||||
|
def _path_comparator(insensitive: bool):
|
||||||
|
if insensitive:
|
||||||
|
return lambda filename, recipient: filename.casefold() == recipient
|
||||||
|
else:
|
||||||
|
return lambda filename, recipient: filename == recipient
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cert_for_email(to_addr, cert_path):
|
||||||
|
insensitive = conf.config_item_equals('default', 'mail_case_insensitive', 'yes')
|
||||||
|
paths_equal = _path_comparator(insensitive)
|
||||||
|
|
||||||
|
LOG.info('Retrieving certificate for %s from %s, insensitive=%s',
|
||||||
|
to_addr, cert_path, insensitive)
|
||||||
|
|
||||||
|
files_in_directory = os.listdir(cert_path)
|
||||||
|
for filename in files_in_directory:
|
||||||
|
file_path = os.path.join(cert_path, filename)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if paths_equal(file_path, to_addr):
|
||||||
|
return (file_path, to_addr)
|
||||||
|
|
||||||
|
# support foo+ignore@bar.com -> foo@bar.com
|
||||||
|
LOG.info(f"An email with topic? {to_addr}")
|
||||||
|
(fixed_up_email, topic) = text.parse_delimiter(to_addr)
|
||||||
|
LOG.info(f'Got {fixed_up_email!r} and {topic!r}')
|
||||||
|
if topic is None:
|
||||||
|
# delimiter not used
|
||||||
|
LOG.info('Topic not found')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
LOG.info(f"Looking up certificate for {fixed_up_email} after parsing {to_addr}")
|
||||||
|
return _get_cert_for_email(fixed_up_email, cert_path)
|
|
@ -0,0 +1,71 @@
|
||||||
|
"""SMTP transport module."""
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
import logging
|
||||||
|
from typing import AnyStr, List
|
||||||
|
|
||||||
|
import lacre.config as conf
|
||||||
|
|
||||||
|
# Mail status constants.
|
||||||
|
#
|
||||||
|
# These are the only values that our mail handler is allowed to return.
|
||||||
|
RESULT_OK = '250 OK'
|
||||||
|
RESULT_ERROR = '500 Could not process your message'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# This is a left-over from old architecture.
|
||||||
|
from_addr = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_sender(fromaddr):
|
||||||
|
"""Set module state: message sender address."""
|
||||||
|
global from_addr
|
||||||
|
LOG.warning('Setting global recipient: %s', fromaddr)
|
||||||
|
from_addr = fromaddr
|
||||||
|
|
||||||
|
|
||||||
|
def send_msg(message: AnyStr, recipients: List[str]):
|
||||||
|
"""Send MESSAGE to RECIPIENTS to the mail relay."""
|
||||||
|
global from_addr
|
||||||
|
LOG.debug('Delivery from %s to %s', from_addr, recipients)
|
||||||
|
|
||||||
|
recipients = [_f for _f in recipients if _f]
|
||||||
|
if recipients:
|
||||||
|
LOG.info(f"Sending email to: {recipients!r}")
|
||||||
|
relay = conf.relay_params()
|
||||||
|
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||||
|
if conf.flag_enabled('relay', 'starttls'):
|
||||||
|
smtp.starttls()
|
||||||
|
smtp.sendmail(from_addr, recipients, message)
|
||||||
|
else:
|
||||||
|
LOG.info("No recipient found")
|
||||||
|
|
||||||
|
|
||||||
|
class SendFrom:
|
||||||
|
"""A class wrapping the transport process."""
|
||||||
|
|
||||||
|
def __init__(self, from_addr):
|
||||||
|
"""Initialise the transport."""
|
||||||
|
self._from_addr = from_addr
|
||||||
|
|
||||||
|
def __call__(self, message: AnyStr, recipients: List[str]):
|
||||||
|
"""Send the given message to all recipients from the list.
|
||||||
|
|
||||||
|
- Message is the email object serialised to str or bytes.
|
||||||
|
- Empty recipients are filtered out before communication.
|
||||||
|
"""
|
||||||
|
recipients = [_f for _f in recipients if _f]
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
LOG.warning("No recipient found")
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.info("Sending email to: %s", recipients)
|
||||||
|
relay = conf.relay_params()
|
||||||
|
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||||
|
|
||||||
|
if conf.flag_enabled('relay', 'starttls'):
|
||||||
|
smtp.starttls()
|
||||||
|
|
||||||
|
smtp.sendmail(self._from_addr, recipients, message)
|
Loading…
Reference in New Issue