I have a need to take an incoming email that matches a specific ruleset in postfix, send it to an external python process, rewrite the email to multiple delivery recipients based upon the postfix postmap table, and re-inject back into the postfix chain. The included python meets that basic requirement except for one, tracking the queueid of the new re-injected email.
The typical method of re-injecting with /usr/sbin/sendmail does not return a usable queueid for the correct process. This causes loss of visibility of the newly created emails (logs have to be manually parse to generate a delivery confirmation report for 3rd parties).
Since the re-injection process is in python, ideally I would like to use the smtplib.Sendmail for that, but it also does not return a queueid.
What I have tried, and kind of works, is using netcat as a subprocess in python (netcat_msg = "helo myserver\nmail from: " + sender + "\nrcpt to: " + recipient + "\ndata\n" + msg.as_string() + "\n\n.\nquit\n") but I feel that's a hack and I get warnings about command sequences in smtp (which is expected since I'm not waiting on the response before issuing the next command).
Is there a way to expose the queueid returned from the remote SMTP server when the process completes? Any recommended approaches to this?
My goal is to log these queueids to a file/api endpoint/whatever so we can determine that incoming email to a#domain.tld was sent to bob#example.com, chris#example.com and track the return status of the destination server.
(Excuse my crude python)
#!/bin/python
#v 2.7.15
import email
import sys
from email.mime.text import MIMEText
import argparse
from subprocess import Popen
from subprocess import PIPE
#ignore this, it was just for debugging
def dump(obj):
for attr in dir(obj):
if hasattr( obj, attr ):
print( "obj.%s = %s" % (attr, getattr(obj, attr)))
def process_message(data, sender, recipient):
msg = email.message_from_string(data)
newaddress = '"{recipient}" <{recipient}>'.format(recipient=recipient)
oldaddress = ''
if msg.has_key('To'):
oldaddress = msg['To']
msg.replace_header('To', newaddress)
else:
msg.add_header('To', newaddress)
oldaddress = newaddress
if msg.has_key('X-Original-To'):
msg.replace_header('X-Original-To', oldaddress)
else:
msg.add_header('X-Original-To', oldaddress)
#print(msg.as_string())
try:
# replace this with a re-inject that can return the queueid
p = Popen(["/usr/sbin/sendmail", "-G", "-i", "-f " + sender, "--", recipient ], stdin=PIPE)
p.communicate(msg.as_string())
# end replacement
# log original queueid, returned queueid and destination email here
except Exception as ex:
exit(69)
def main():
parser = argparse.ArgumentParser(description='To field replacement for Email MIME.')
parser.add_argument('--from', dest="sender", help="From email address", required=True)
parser.add_argument('--recipient', dest="recipients", help="Recipient address to replace in To field (N+1)", nargs='+', required=True)
args = parser.parse_args()
#dump(args)
data = sys.stdin.readlines()
data = ''.join(data)
for recipient in args.recipients:
#print(recipient)
process_message(data, args.sender, recipient)
exit(0)
main()
Related
I have verified the following
AWS SES isn't in Sandbox. I can send email via console to non-verified email ids.
My Lambda function has a role attached with full access to SES and Lambda (since its initial basic testing gave full permissions)
The following below a basic code from AWS documentation, just hard coded my email id. Yet I can't receive any email. The lambda code runs successfully but I don't receive emails.
import json
import os
import boto3
from botocore.exceptions import ClientError
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
print('Loading function')
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
#print("value1 = " + event['key1'])
#print("value2 = " + event['key2'])
#print("value3 = " + event['key3'])
#return event['key1'] # Echo back the first key value
#raise Exception('Something went wrong')
SENDER = "[redacted email]"
RECIPIENT = event['email']
CONFIGURATION_SET = "ConfigSet"
AWS_REGION = "us-east-2"
SUBJECT = "Contact Us Form Details"
# The email body for recipients with non-HTML email clients.
BODY_TEXT = "Hello,\r\nPlease see the attached file for a list of customers to contact."
# The HTML body of the email.
BODY_HTML = """\
<html>
<head></head>
<body>
<h1>Hello!</h1>
<p>Please see the attached file for a list of customers to contact.</p>
</body>
</html>
"""
# The character encoding for the email.
CHARSET = "utf-8"
# Create a new SES resource and specify a region.
client = boto3.client('ses',region_name='us-east-2')
# Create a multipart/mixed parent container.
msg = MIMEMultipart('mixed')
# Add subject, from and to lines.
msg['Subject'] = "Contact Us Form Details"
msg['From'] ="[redacted email]"
msg['To'] = "[redacted email]"
# Create a multipart/alternative child container.
msg_body = MIMEMultipart('alternative')
# Encode the text and HTML content and set the character encoding. This step is
# necessary if you're sending a message with characters outside the ASCII range.
textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET)
htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET)
# Add the text and HTML parts to the child container.
msg_body.attach(textpart)
msg_body.attach(htmlpart)
# Define the attachment part and encode it using MIMEApplication.
#att = MIMEApplication(open(ATTACHMENT, 'rb').read())
# Add a header to tell the email client to treat this part as an attachment,
# and to give the attachment a name.
#att.add_header('Content-Disposition','attachment',filename=os.path.basename(ATTACHMENT))
# Attach the multipart/alternative child container to the multipart/mixed
# parent container.
msg.attach(msg_body)
# Add the attachment to the parent container.
#msg.attach(att)
print(msg)
try:
#Provide the contents of the email.
response = client.send_raw_email(
Source="[redacted email]",
Destinations=[
"[redacted email]"
],
RawMessage={
'Data':msg.as_string(),
},
#ConfigurationSetName=CONFIGURATION_SET
)
# Display an error if something goes wrong.
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])
Attaching my cloud watch logs for reference
If your code is really what you have displayed to us, then the reason that it is not sending the email is because half your code is not being executed.
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
#print("value1 = " + event['key1'])
#print("value2 = " + event['key2'])
#print("value3 = " + event['key3'])
#return event['key1'] # Echo back the first key value
#raise Exception('Something went wrong')
SENDER = "[redacted email]"
RECIPIENT = event['email']
CONFIGURATION_SET = "ConfigSet"
AWS_REGION = "us-east-2"
SUBJECT = "Contact Us Form Details"
# The email body for recipients with non-HTML email clients.
BODY_TEXT = "Hello,\r\nPlease see the attached file for a list of customers to contact."
When AWS Lambda executes the function, it calls lambda_handler(). As per Python formatting, it will execute all indented lines since they form part of the function. This includes your print() statement.
However, starting with the BODY_TEXT = ... line, there is no indenting. This means that the code is part of the "main" program, and not part of the lambda_handler() function. It would be executed when the Lambda container is first instantiated, but not when the function is triggered.
Bottom line: If this is your actual code, you need to fix your indents.
If you are not getting any errors from when executing the lambda, then most probably you are not hitting the SES API. From what I see in your code, you are missing an Access Key Id and Secret Access Key. Try configuring your boto client like this:
client = boto3.client(
'ses',
region_name=region,
aws_access_key_id='aws_access_key_string',
aws_secret_access_key='aws_secret_key_string'
)
Also make sure that your lambda is deployed in the same region as your SES. I see you are using us-east-2.
Another discrepancy I see, which is also in the documentation is that in the official AWS documentation, Destinations is actually Destination. Try it without the 's'.
Can you also paste the cloudwatch logs for the lambda. I see it should print the message Id upon success. Does it?
I am currently trying to write a script to send off a request token, I have the header, and the claimset, but I don't understand the signature! OAuth requires my private key to be encrypted with SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function), but the closest I could find was RSAES-PKCS1-v1_5 (has RSA, and the SHA-256 hash). I followed the example, and tweaked it, so I could get it set, but heres my dillema:
signature = ""
h = SHA.new (signature)
key = RSA.importKey(open('C:\Users\Documents\Library\KEY\My Project 905320c6324f.json').read())
cipher = PKCS1_v1_5.new(key)
ciphertext = cipher.encrypt(message+h.digest())
print(ciphertext)
I'm a bit lost, the JSON file I was given has both public key, and private, do I copy and paste the private key into the signature variable (it gave me a invalid syntax)? Or do I past the directory again? I am so lost, and way over my head haha. I am currently running Python 3.4, with pyCrypto for the signature.
Based on what you've said below about wanting to write a command system using gmail, I wrote a simple script to do this using IMAP. I think this is probably simpler than trying to use Google APIs for a single user, unless you were wanting to do that simply for the exercise.
import imaplib, logging
from time import sleep
USERNAME = 'YOUR_USERNAME_HERE' # For gmail, this is your full email address.
PASSWORD = 'YOUR_PASSWORD_HERE'
CHECK_DELAY = 60 # In seconds
LOGGING_FORMAT = '%(asctime)s %(message)s'
logging.basicConfig(filename='imapTest.log', format=LOGGING_FORMAT, level=logging.INFO)
logging.info("Connecting to IMAP server...")
imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login(USERNAME, PASSWORD)
logging.info("Connected to IMAP server.")
def get_command_messages():
logging.info("Checking for new commands.")
imap.check()
# Search the inbox (server-side) for messages containing the subject 'COMMAND' and which are from you.
# Substitute USERNAME below for the sending email address if it differs.
typ, data = imap.search(None, '(FROM "%s" SUBJECT "COMMAND")' %(USERNAME))
return data[0]
def delete_messages(message_nums):
logging.info("Deleting old commands.")
for message in message_nums.split():
imap.store(message, '+FLAGS', '\\DELETED')
imap.expunge()
# Select the inbox
imap.select()
# Delete any messages left over that match commands, so we are starting 'clean'.
# This probably isn't the nicest way to do this, but saves checking the DATE header.
message_nums = get_command_messages()
delete_messages(message_nums)
try:
while True:
sleep(CHECK_DELAY)
# Get the message body and sent time. Use BODY.PEEK instead of BODY if you don't want to mark the message as read, but we're deleting it anyway below.
message_nums = get_command_messages()
if message_nums:
# search returns space-separated message IDs, but we need them comma-separated for fetch.
typ, messages = imap.fetch(message_nums.replace(' ', ','), '(BODY[TEXT])')
logging.info("Found %d commands" %(len(messages[0])))
for message in messages[0]:
# You now have the message body in the message variable.
# From here, you can check against it to perform commands, e.g:
if 'shutdown' in message:
print("I got a shutdown command!")
# Do stuff
delete_messages(message_nums)
finally:
try:
imap.close()
except:
pass
imap.logout()
If you're set on using the Gmail API, though, Google strongly encourage you to use their existing Python library rather than attempt to do full authentication etc. yourself as you appear to be. With that, it should - more or less - be a case of replacing the imap calls above with the relevant Gmail API ones.
I'm working on a script in Python that checks an IP address against a blacklist and sends an email only if the IP shows up on the list. The script will be setup to be run every 15 minutes, but I only want it to send an email if the IP is on the list and an email hasn't been sent in the last 24 hours. Current code:
import sys
import subprocess
import smtplib
import datetime
username = ''
password = ''
fromaddr = ''
toaddr = ''
server = smtplib.SMTP(host=,port=)
server.starttls()
server.ehlo()
server.esmtp_features["auth"] = "LOGIN PLAIN"
server.login(username,password)
sentFolder = server.select("SENT",readonly=TRUE)
recentSent = sentFolder["Date"]
OneDayAgo = date.today()-timedelta(days=1)
msg = ''
staticIPAddress = ''
dnsHostname = staticIPAddress + ".bl.spamcop.net"
p = subprocess.check_output("nslookup " + dnsHostname1,stderr=subprocess.STDOUT,shell=False)
if ('Non-existent' not in str(p) and recentSent < OneDayAgo):
server.sendmail(fromaddr, toaddrs, msg)
The error I run into occurs at:
sentFolder = server.select("SENT",readonly=TRUE)
The error code is:
AttributeError: 'SMTP' object has no attribute 'select'
I've tested the rest of the script (without that piece and without the recentSent < OneDayAgo pieces) and it seems to work fine.
Any help in figuring out how to make the "only send if not sent within the last 24 hours" piece work would be really appreciated.
In order to know if you've sent email in the previous 24 hours, you'll need to make a record of sending the email. You might store that information in a text file, an IMAP folder, a database, through a web app, or many other ways. How you store that data is your design decision.
Here is one possibility, in which the timestamp is stored in the modification date of a local file.
#UNTESTED EXAMPLE CODE
def create_timestamp():
with open("tsfile", "w") as fp:
fp.write("now")
def time_since_last_timestamp():
return time.time() - os.path.getmtime("tsfile")
...
if 'Non-existent' not in str(p) and time_since_last_timestamp() > 86400:
server.sendmail(...)
create_timestamp()
To determine whether or not an email has been sent in the last 24 hours, you might want to program your script to examine the mail server logs. You didn't mention which MTA you are using, but all that I know of log messages in and out.
I am trying to fetch emails on a gmail account using twisted, and to say the least it has been a pain, looking at email is their a clear explanation and structure (it seems hacked together at best). I am trying to grab attachments yet the attachment isn't any where in sight.
I am using the example IMAP Client from twisted and modified it, I am using fetchAll('1:') to get the email and then getting the first email but I can't find the email attachment that is on that email (I checked it is there in google). Also what is with the 1: and I can't seem to find any thing that actually explains email (as though no one understands it)
So Stackoverflow, what am I missing?
Code
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Simple IMAP4 client which displays the subjects of all messages in a
particular mailbox.
"""
import sys
from twisted.internet import protocol
from twisted.internet import ssl
from twisted.internet import defer
from twisted.internet import stdio
from twisted.mail import imap4
from twisted.protocols import basic
from twisted.python import util
from twisted.python import log
class TrivialPrompter(basic.LineReceiver):
from os import linesep as delimiter
promptDeferred = None
def prompt(self, msg):
assert self.promptDeferred is None
self.display(msg)
self.promptDeferred = defer.Deferred()
return self.promptDeferred
def display(self, msg):
self.transport.write(msg)
def lineReceived(self, line):
if self.promptDeferred is None:
return
d, self.promptDeferred = self.promptDeferred, None
d.callback(line)
class SimpleIMAP4Client(imap4.IMAP4Client):
"""
A client with callbacks for greeting messages from an IMAP server.
"""
greetDeferred = None
def serverGreeting(self, caps):
self.serverCapabilities = caps
if self.greetDeferred is not None:
d, self.greetDeferred = self.greetDeferred, None
d.callback(self)
class SimpleIMAP4ClientFactory(protocol.ClientFactory):
usedUp = False
protocol = SimpleIMAP4Client
def __init__(self, username, onConn):
self.ctx = ssl.ClientContextFactory()
self.username = username
self.onConn = onConn
def buildProtocol(self, addr):
"""
Initiate the protocol instance. Since we are building a simple IMAP
client, we don't bother checking what capabilities the server has. We
just add all the authenticators twisted.mail has. Note: Gmail no
longer uses any of the methods below, it's been using XOAUTH since
2010.
"""
assert not self.usedUp
self.usedUp = True
p = self.protocol(self.ctx)
p.factory = self
p.greetDeferred = self.onConn
p.registerAuthenticator(imap4.PLAINAuthenticator(self.username))
p.registerAuthenticator(imap4.LOGINAuthenticator(self.username))
p.registerAuthenticator(
imap4.CramMD5ClientAuthenticator(self.username))
return p
def clientConnectionFailed(self, connector, reason):
d, self.onConn = self.onConn, None
d.errback(reason)
def cbServerGreeting(proto, username, password):
"""
Initial callback - invoked after the server sends us its greet message.
"""
# Hook up stdio
tp = TrivialPrompter()
stdio.StandardIO(tp)
# And make it easily accessible
proto.prompt = tp.prompt
proto.display = tp.display
# Try to authenticate securely
return proto.authenticate(password
).addCallback(cbAuthentication, proto
).addErrback(ebAuthentication, proto, username, password
)
def ebConnection(reason):
"""
Fallback error-handler. If anything goes wrong, log it and quit.
"""
log.startLogging(sys.stdout)
log.err(reason)
return reason
def cbAuthentication(result, proto):
"""
Callback after authentication has succeeded.
Lists a bunch of mailboxes.
"""
return proto.list("", "*"
).addCallback(cbMailboxList, proto
)
def ebAuthentication(failure, proto, username, password):
"""
Errback invoked when authentication fails.
If it failed because no SASL mechanisms match, offer the user the choice
of logging in insecurely.
If you are trying to connect to your Gmail account, you will be here!
"""
failure.trap(imap4.NoSupportedAuthentication)
return proto.prompt(
"No secure authentication available. Login insecurely? (y/N) "
).addCallback(cbInsecureLogin, proto, username, password
)
def cbInsecureLogin(result, proto, username, password):
"""
Callback for "insecure-login" prompt.
"""
if result.lower() == "y":
# If they said yes, do it.
return proto.login(username, password
).addCallback(cbAuthentication, proto
)
return defer.fail(Exception("Login failed for security reasons."))
def cbMailboxList(result, proto):
"""
Callback invoked when a list of mailboxes has been retrieved.
"""
result = [e[2] for e in result]
s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)])
if not s:
return defer.fail(Exception("No mailboxes exist on server!"))
return proto.prompt(s + "\nWhich mailbox? [1] "
).addCallback(cbPickMailbox, proto, result
)
def cbPickMailbox(result, proto, mboxes):
"""
When the user selects a mailbox, "examine" it.
"""
mbox = mboxes[int(result or '1') - 1]
return proto.examine(mbox
).addCallback(cbExamineMbox, proto
)
def cbExamineMbox(result, proto):
"""
Callback invoked when examine command completes.
Retrieve the subject header of every message in the mailbox.
"""
# FETCH ALL HEADERS? WHERE IS A ONE FOR AN ATTACHMENT
return proto.fetchAll('1:*').addCallback(cbFetch, proto)
def cbFetch(result, proto):
"""
Finally, display headers.
"""
if result:
keys = result.keys()
keys.sort()
k = keys[-1]
proto.display('%s %s' % (k, result[k]))
else:
print "Hey, an empty mailbox!"
return proto.logout()
def cbClose(result):
"""
Close the connection when we finish everything.
"""
from twisted.internet import reactor
reactor.stop()
def main():
hostname = raw_input('IMAP4 Server Hostname: ')
port = raw_input('IMAP4 Server Port (the default is 143, 993 uses SSL): ')
username = raw_input('IMAP4 Username: ')
password = util.getPassword('IMAP4 Password: ')
onConn = defer.Deferred(
).addCallback(cbServerGreeting, username, password
).addErrback(ebConnection
).addBoth(cbClose)
factory = SimpleIMAP4ClientFactory(username, onConn)
from twisted.internet import reactor
if port == '993':
reactor.connectSSL(hostname, int(port), factory, ssl.ClientContextFactory())
else:
if not port:
port = 143
reactor.connectTCP(hostname, int(port), factory)
reactor.run()
if __name__ == '__main__':
main()
First off, imap4 is a (perhaps needlessly) complex protocol for mail handling, and twisted's client implementation is (necessarily) complex to fully support that protocol. To get much of anywhere, you should consider spending some time reading the standard that explains the protocol: rfc3501 and the relevent parts of twisted's api.
That said, it looks like you're using IMAP4Client.fetchAll(), which, paradoxically, fetches "envelope" data, the headers and metadata about the message. the comparable call that returns the body of the email, too, is actually fetchFull().
Thanks to the pointers to RFC3501 and RFC822 by SingleNegationElimination and Jean-Paul, I think I have a better understanding of how to achieve this.
The solution I found is to the API fetchSpecific source code of imap4. You can specify which "part" of the message (i.e. a mail in your inbox) to retrieve. For attachment, the content of the attached file(s) is embedded in the TEXT part and encoded with base64. In my particular example, there is a pdf attachment in the mail, and
proto.fetchSpecific(imap4.MessageSet(247), uid=True, headerType='TEXT').addCallback(cbViewAttachment, proto)
def cbViewAttachment(result, proto):
for k, value in result.items():
print(value[0][4][:400])
...
gives something like
------_CANON_2007111239350128_
Content-Type: Application/pdf;
name="0128_20200711123935_001.pdf"
Content-Disposition: attachment;
filename="0128_20200711123935_001.pdf"
Content-Transfer-Encoding: base64
JVBERi0xLjYKJeLjz9MNCjEgMCBvYmoKPDwgCi9DcmVhdGlvbkRhdGUgKEQ6MjAyMDA3MTEyMDM5
MThaMDAnMDAnKQovQ3JlYXRvciAoXDM3NlwzNzdcMDAwQ1wwMDBhXDAwMG5cMDAwb1wwMDBuXDAw
MCBcMDAwTVwwMDBGXDAwMDJcMDAwNFw
Some quick explanation. The above code does fetch the TEXT part of the message with UID = 247. Upon success of the call to fetchSpecific, cbDownloadAttachment is called, where the first argument is a dictionary of TEXT parts - one entry for each of the specified message. In this case there is only one message being specified, so the result will just be a dictionary with one single entry corresponding to the UID = 247 message.
The actual attachment content is embedded in the [0][4] of the value of the entry. It looks like above. It is almost obvious that the JVB... part is the base64-encoded content. I tried to decode the first small portion of it with base64 and it looks like
%PDF-1.6\n%\xe2\xe3
which means that this is actually the beginning of a PDF file.
You can then write the content to a local file so as to "download" the attachment. In the above example it is basically
with open('your/file/name', 'wb') as f:
f.write(base64.urlsafe_b64decode(value[0][4][213:]))
Note that 213 is hard-coded as the start of the content. In the real-world case you should definitely parse the TEXT a bit to find it out.
Hope this helps someone in the similar situation as me.
I have the logging module MemoryHandler set up to queue debug and error messages for the SMTPHandler target. What I want is for an email to be sent when the process errors that contains all debug statements up to that point (one per line). What I get instead is a separate email for every debug message.
This seems like it should be trivial, and part of the logging package, but I can't find anything about it, no examples, nothing on Google.
log = logging.getLogger()
log.setLevel(logging.DEBUG)
debug_format = logging.Formatter("%(levelname)s at %(asctime)s in %(filename)s (line %(lineno)d):: %(message)s")
# write errors to email
error_mail_subject = "ERROR: Script error in %s on %s" % (sys.argv[0], os.uname()[1])
error_mail_handler = logging.handlers.SMTPHandler(SMTP_HOST, 'errors#'+os.uname()[1], [LOG_EMAIL], error_mail_subject)
error_mail_handler.setLevel(logging.ERROR)
#error_mail_handler.setLevel(logging.DEBUG)
error_mail_handler.setFormatter(debug_format)
# buffer debug messages so they can be sent with error emails
memory_handler = logging.handlers.MemoryHandler(1024*10, logging.ERROR, error_mail_handler)
memory_handler.setLevel(logging.DEBUG)
# attach handlers
log.addHandler(memory_handler)
log.addHandler(error_mail_handler)
Related to this:
Do I need to add the error_mail_handler to the logger explicitly if it is a target of memory_handler anyway?
Should error_mail_handler be set to DEBUG or ERROR target? Does it even need a target when it is being fed from memory_handler?
Would love to see some working code from anyone who has solved this problem.
You might want to use or adapt the BufferingSMTPHandler which is in this test script.
In general, you don't need to add a handler to a logger if it's the target of a MemoryHandler handler which has been added to a logger. If you set the level of a handler, that will affect what the handler actually processes - it won't process anything which is less severe than its level setting.
Instead of buffering for email, consider posting unbuffered to a message stream on a messaging app, e.g. on Matrix, Discord, Slack, etc. Having said that, I wrote my own beastly thread-safe implementation of BufferingSMTPHandler (backup link) which sends emails from a separate thread. The primary goal is to not block the main thread.
As written, it uses two queues - this seemed necessary in order to implement some useful class-level parameters that are defined in the "Configurable parameters" section of the code. Although you can use the code as-is, it's probably better if you study and use it to write your own class.
Issues:
Some class-level parameters can perhaps be instance-level instead.
Either threading.Timer or the signal module could perhaps be used to avoid loops that run forever.
If you are using django - here is simple buffering handler, which will use standard django email methods:
import logging
from django.conf import settings
from django.core.mail import EmailMessage
class DjangoBufferingSMTPHandler(logging.handlers.BufferingHandler):
def __init__(self, capacity, toaddrs=None, subject=None):
logging.handlers.BufferingHandler.__init__(self, capacity)
if toaddrs:
self.toaddrs = toaddrs
else:
# Send messages to site administrators by default
self.toaddrs = zip(*settings.ADMINS)[-1]
if subject:
self.subject = subject
else:
self.subject = 'logging'
def flush(self):
if len(self.buffer) == 0:
return
try:
msg = "\r\n".join(map(self.format, self.buffer))
emsg = EmailMessage(self.subject, msg, to=self.toaddrs)
emsg.send()
except Exception:
# handleError() will print exception info to stderr if logging.raiseExceptions is True
self.handleError(record=None)
self.buffer = []
In django settings.py you will need to configure email and logging like this:
EMAIL_USE_TLS = True
EMAIL_PORT = 25
EMAIL_HOST = '' # example: 'smtp.yandex.ru'
EMAIL_HOST_USER = '' # example: 'user#yandex.ru'
EMAIL_HOST_PASSWORD = ''
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
LOGGING = {
'handlers': {
...
'mail_buffer': {
'level': 'WARN',
'capacity': 9999,
'class': 'utils.logging.DjangoBufferingSMTPHandler',
# optional:
# 'toaddrs': 'admin#host.com'
# 'subject': 'log messages'
}
},
...
}
For this purpose I use the BufferingSMTPHandler suggested by Vinay Sajip with one minor tweak: I set the buffer length to something really big (say 5000 log records) and manualy call the flush method of the handler every some seconds and after checking for internet conectivity.
# init
log_handler1 = BufferingSMTPHandler(
'smtp.host.lala', "from#test.com", ['to#test.com'], 'Log event(s)',5000)
...
logger.addHandler(log_handler1)
...
# main code
...
if internet_connection_ok and seconds_since_last_flush>60:
log_handler1.flush() # send buffered log records (if any)
Updated Vinay Sajip's answer for python3.
import logging
from logging.handlers import BufferingHandler
class BufferingSMTPHandler(BufferingHandler):
def __init__(self, mailhost, fromaddr, toaddrs, subject, capacity):
logging.handlers.BufferingHandler.__init__(self, capacity)
self.mailhost = mailhost
self.mailport = None
self.fromaddr = fromaddr
self.toaddrs = toaddrs
self.subject = subject
self.setFormatter(logging.Formatter("%(asctime)s %(levelname)-5s %(message)s"))
def flush(self):
if len(self.buffer) > 0:
try:
import smtplib
port = self.mailport
if not port:
port = smtplib.SMTP_PORT
smtp = smtplib.SMTP(self.mailhost, port)
msg = '''From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n'''.format(
self.fromaddr,
",".join(self.toaddrs),
self.subject
)
for record in self.buffer:
s = self.format(record)
print (s)
msg = msg + s + "\r\n"
smtp.sendmail(self.fromaddr, self.toaddrs, msg)
smtp.quit()
except:
self.handleError(None) # no particular record
self.buffer = []
#update for #Anant
if __name__ == '__main__'
buff_smtp_handler=BufferingSMTPHandler(...your args)
buff_smtp_handler.setLevel(logging.ERROR)
handlers=[buff_smtp_handler]
logging.basicConfig(handlers=handlers)
I think the point about the SMTP logger is that it is meant to send out a significant log message functioning as some kind of alert if sent to a human recipient or else to be further processed by an automated recipient.
If a collection of log messages is to be sent by email then that constitutes a report being sent at the end of execution of a task and writing that log to a file and then emailing the file would seem to be a reasonable solution.
I took a look at the basic FileHandler log handler and how to build a mechanism to write to a temp file then attach that temp file when the script exits.
I found the "atexit" module that allows for a method to be registered that will be executed against an object when the script is exiting.
import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
import os
from email import encoders
import uuid
# atexit allows for a method to be set to handle an object when the script exits
import atexit
filename = uuid.uuid4().hex
class MailLogger:
def __init__(self, filePath, smtpDict):
self.filePath = filePath
self.smtpDict = smtpDict
# Generate random file name
filename = '%s.txt' % ( uuid.uuid4().hex )
# Create full filename
filename = '%s/%s' % (filePath,filename)
self.filename = filename
self.fileLogger = logging.getLogger('mailedLog')
self.fileLogger.setLevel(logging.INFO)
self.fileHandler = logging.FileHandler(filename)
self.fileHandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
self.fileHandler.setFormatter(formatter)
self.fileLogger.addHandler(self.fileHandler)
atexit.register(self.mailOut)
def mailOut(self):
'''
Script is exiting so time to mail out the log file
"emailSettings": {
"smtpServer" : "smtp.dom.com",
"smtpPort" : 25,
"sender" : "sender#dom.com>",
"recipients" : [
"recipient#dom.com"
],
"subject" : "Email Subject"
},
'''
# Close the file handler
smtpDict = self.smtpDict
self.fileHandler.close()
msg = MIMEMultipart('alternative')
s = smtplib.SMTP(smtpDict["smtpServer"], smtpDict["smtpPort"] )
msg['Subject'] = smtpDict["subject"]
msg['From'] = smtpDict["sender"]
msg['To'] = ','.join(smtpDict["recipients"])
body = 'See attached report file'
content = MIMEText(body, 'plain')
msg.attach(content)
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(open(self.filename, 'rb').read())
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(self.filename))
msg.attach(attachment)
s.send_message(msg)
s.quit()
My basic test script is:
from EmailLogRpt import MailLogger
import time
smtpDict = {
"smtpServer" : "smtp.dom.com",
"smtpPort" : 25,
"sender" : "sender#dom.com",
"recipients" : [
"recpient#dom.com>"
],
"subject" : "Email Subject"
}
myMailLogger = MailLogger("/home/ed/tmp",smtpDict).fileLogger
myMailLogger.info("test msg 1")
time.sleep(5)
myMailLogger.info("test msg 2")
Hope this helps somebody.