Allowing users to manage frequency of emails? - Python Bulk Mailer - python

I have a task to create a bulk mailer in python which sends bulk email content to a list of subscribers - how would I go about inputting code to allow the subscribers to manage the frequency and content of emails they receive?
import pandas as pd
import smtplib
# reading excel email list + retrieving the values
e = pd.read_excel(r"C:\Users\****\OneDrive\Desktop\emailList.xlsx")
email = e['Email'].values
# setting up server to send mail
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
server.login("bulkmailer****#gmail.com", "*****")
msg = "Hi there! Check out these exclusive offers tailored just for you!"
subject = "Exclusive Offers Inside"
body = "Subject : {}\n\n{}".format(subject, msg)
# for loop for server to send emails from server to email list
for email in email:
server.sendmail("bulkmailer****#gmail.com", email, body)
server.quit()

The code you have provided has the effect of sending a single message to every one of your subscribers. To have any "frequency" to speak of, you need to run this program occasionally -- for example, you can set up a cron job (or a Windows equivalent) that executes it once every X time -- say, once per minute.
Won't that mean your subscribers will get spammed with messages once per minute? It would, unless we add something else: a way to record when the message has been sent, or, equivalently, when the next message is due.
Specifically, along with each address, you need to store: the content of the message you'd like to send to them this time; the interval with which you intend to send the messages; and the last time that we sent a message to that address.
For this, normal applications use a database. You are using Pandas Dataframes, which probably have sufficient capabilities, but they're definitely harder to use for this. Since you have said in the comments that this is a homework question, and also because I have no experience with Pandas, I will instead provide some ORM-like pseudocode.
from dataclasses import dataclass
import database
import time
import mailer
#dataclass
class DatabaseRow:
""" Single row in database of email addresses and associated data """
email: str # email address to send message to
subject: str # message subject
body: str # message body
send_interval: int # or float -- number of seconds between each message
next_send_at: Optional[int] # int or None (or float or None); Unix time at which to send next message; if None, send immediately
for row in database.get_all_rows():
current_time = time.time() # this returns Unix time: the number of seconds since 1 Jan 1970
if row.next_send_at is None or current_time < row.next_send_at:
# it is not time to send it to them yet; don't do anything
continue
mailer.send(row.address, row.subject, row.body)
row.next_send_at = current_time + row.send_interval # the next time we'll send a message is (send_interval) away from now
row.save()

Related

Transfer an email with Python

I've tried with no conclusions to resend emails with Python.
Once I've logged in SMTP and IMAP with TLS, this is what I have written:
status, data = self._imapserver.fetch(id, "(RFC822)")
email_data = data[0][1]
# create a Message instance from the email data
message = email.message_from_string(email_data)
# replace headers (could do other processing here)
message.replace_header("From", 'blablabla#bliblibli.com')
message.replace_header("To", 'blobloblo#blublublu.com')
self._smtpserver.sendmail('blablabla#bliblibli.com', 'blobloblo#blublublu.com', message.as_string())
But the problem is that the variable data doesn't catch the information from the email, even if the ID is the one I need.
It tells me:
b'The specified message set is invalid.'
How can I transfer an email with Python?
Like the error message says, whatever you have in id is invalid. We don't know what you put there, so all we can tell you is what's already in the error message.
(Also, probably don't use id as a variable name, as you will shadow the built-in function with the same name.)
There are additional bugs further on in your code; you need to use message_from_bytes if you want to parse it, though there is really no need to replace the headers just to resend it.
status, data = self._imapserver.fetch(correct_id, "(RFC822)")
self._smtpserver.sendmail('blablabla#bliblibli.com', 'blobloblo#blublublu.com', data[0][1])
If you want to parse the message, you should perhaps add a policy argument; this selects the modern EmailMessage API which was introduced in Python 3.6.
from email.policy import default
...
message = email.message_from_bytes(data[0][1], policy=default)
message["From"] = "blablabla#bliblibli.com"
message["To"] = "blobloblo#blublublu.com"
self._smtpserver.send_message(message)
The send_message method is an addition to the new API. If the message could contain other recipient headers like Cc:, Bcc: etc, perhaps using the good old sendmail method would be better, as it ignores the message's headers entirely.

How can I get the date recieved / sent from email in python

I have a program that needs to read in emails and validate if they are from this month, before continuing.
I obtain the email info via the following code
import email
import smtplib
import imaplib
mail = imaplib.IMAP4_SSL('redacted', 993)
mail.login(username, bytes(password).decode('utf-8')) #password is bytes that have been decrypted
msg_data2 = [] #My template allows for multiple email data to be appended
mailbox_data = mail.list()
mail.select('INBOX', readonly=True)
result, msg_ids = mail.search(None, f'(SEARCH CRITERIA REDACTED)')
lister = msg_ids[0].split()
most_recent = lister[-1]
result2, msg_data = mail.fetch(most_recent, '(RFC822)')
msg_data2.append(msg_data)
raw = email.message_from_bytes(msg_data[0][1])
from here im able to get attachments from my emails matching the search criteria, and previously, vendors would name the files properly with the month their jobs ran. Now some are not, so Im attempting to just check the date the email was sent or received.
You can get the sending date from the email's 'date' header.
from email import utils
...
raw = email.message_from_bytes(msg_data[0][1])
datestring = raw['date']
print(datestring)
# Convert to datetime object
datetime_obj = utils.parsedate_to_datetime(datestring)
print(repr(datetime_obj))
The Date: header is inserted by the sender, and may or may not be accurate. For example, when I write an email and place it in the outbox, it gets the date and time of me placing it in the outbox in the Date: header. The header remains the same even if I only send the email hours (or possibly days) later.
This still doesn't say anything on when it was received. It may be stuck in transit for days. For that it depends on your mail client. For example, Claws inserts a X-Received header when it fetches mail, and that will have the timestamp when Claws downloaded the email from the server to your local machine. This may be minutes or even days after it arrived in your inbox.
To check when the email actually was received by your email provider, look at the Received: headers. The top header is from your (provider's) mail server. It should end in a time stamp, with a semicolon separating the time stamp from the rest of the header.
All RFC 5322 time stamps can be parsed with email.utils.parsedate.
So the code would be something along those lines:
from email import utils
mail = "..."
sent = mail['date']
print(f"Date header: {sent}")
received = mail['Received'][0]
received = received.split(";")[-1]
print(f"Received: {received}")
sent_ts = utils.parsedate(sent_date)
received_ts = utils.parsedate(received_ts)
time_in_transit = received_ts = sent_ts
print(f"Sent {sent_ts}, received {received_ts}, took {time_in_transit}")

parse and decode mail text in aiosmtpd, perform string substitution, and reinject

I began with smtpd in order to process mailqueue, parse inbound emails and send them back to recipients (using smtpdlib.sendmail).
I switched to aiosmtpd since i needed multithread processing (while smtpd is single-threaded, and besides that looks like discontinued).
By the way I'm puzzled by aiosmtpd management of mail envelope contents, that seems much more granular than before, so good if you need really fine tuning, but somewhat oversized if you just want to process body without modifying the rest.
To make an example, smtpd process_message method just needed data_decode=True parameter to process and decode mail body without touching anything, while aiosmtpd HANDLE_data method seems unable to automagically decode mail envelope and often gives exceptions with embedded images, attachments, and so on...
EDIT added code examples, smtpd first: following code will instantiate smtp server waiting for mail on port 10025 and delivering to 10027 via smtplib (both localhost). It is safe to work on data variable (basically perform string substitutions, my goal) for all kind of mail (text/html based, with embedded images, attachments...)
class PROXY_SMTP(smtpd.SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data, decode_data=True):
server = smtplib.SMTP('localhost', 10027)
server.sendmail(mailfrom, rcpttos, data)
server.quit()
server = PROXY_SMTP(('127.0.0.1', 10025), None)
asyncore.loop()
Previous code works well but in a single thread fashion (= 1 mail at once), so i switched to aiosmtpd to have concurrent mail processing. Same example with aiosmtpd would be roughly:
class MyHandler:
async def handle_DATA(self, server, session, envelope):
peer = session.peer
mailfrom = envelope.mail_from
rcpttos = envelope.rcpt_tos
data = envelope.content.decode()
server = smtplib.SMTP('localhost', 10027)
server.sendmail(mailfrom, rcpttos, data)
server.quit()
my_handler = MyHandler()
async def main(loop):
my_controller = Controller(my_handler, hostname='127.0.0.1', port=10025)
my_controller.start()
loop = asyncio.get_event_loop()
loop.create_task(main(loop=loop))
try:
loop.run_forever()
This code works well for text emails, but will give exceptions when decoding envelope.content with any complex mail (mime content, attachments...)
How could I parse and decode mail text in aiosmtpd, perform string substitution as I did with smtpd, and reinject via smtplib?
You are calling decode() on something whose encoding you can't possibly know or predict in advance. Modifying the raw RFC5322 message is extremely problematic anyway, because you can't easily look inside quoted-printable or base64 body parts if you want to modify the contents. Also watch out for RFC2047 encapsulation in human-visible headers, file names in RFC2231 (or some dastardly non-compliant perversion - many clients don't get this even almost right) etc. See below for an example.
Instead, if I am guessing correctly what you want, I would parse it into an email object, then take it from there.
from email import message_from_bytes
from email.policy import default
class MyHandler:
async def handle_DATA(self, server, session, envelope):
peer = session.peer
mailfrom = envelope.mail_from
rcpttos = envelope.rcpt_tos
message = message_from_bytes(envelope.content, policy=default)
# ... do things with the message,
# maybe look into the .walk() method to traverse the MIME structure
server = smtplib.SMTP('localhost', 10027)
server.send_message(message, mailfrom, rcpttos)
server.quit()
return '250 OK'
The policy argument selects the modern email.message.EmailMessage class which replaces the legacy email.message.Message class from Python 3.2 and earlier. (A lot of online examples still promote the legacy API; the new one is more logical and versatile, so you want to target that if you can.)
This also adds the missing return statement which each handler should provide as per the documentation.
Here's an example message which contains the string "Hello" in two places. Because the content-transfer-encoding obscures the content, you need to analyze the message (such as by parsing it into an email object) to be able to properly manipulate it.
From: me <me#example.org>
To: you <recipient#example.net>
Subject: MIME encapsulation demo
Mime-Version: 1.0
Content-type: multipart/alternative; boundary="covfefe"
--covfefe
Content-type: text/plain; charset="utf-8"
Content-transfer-encoding: quoted-printable
You had me at "H=
ello."
--covfefe
Content-type: text/html; charset="utf-8"
Content-transfer-encoding: base64
PGh0bWw+PGhlYWQ+PHRpdGxlPkhlbGxvLCBpcyBpdCBtZSB5b3UncmUgbG9va2luZyBmb3I/PC
90aXRsZT48L2hlYWQ+PGJvZHk+PHA+VGhlIGNvdiBpbiB0aGUgZmUgZmU8L3A+PC9ib2R5Pjwv
aHRtbD4K
--covfefe--
The OP incorrectly added this text to the question; I'm moving it here as a (half) answer.
--- SOLVED ---
This is what i gotten so far, minor adjustments are still needed (mainly for mime content separate handling and "rebuilding") but this solves my main problem: receive mail on separated threads, make room for text processing, sleep for fixed amount of time before final delivery. Thanks to tripleee answers and comments I found correct way.
import asyncio
from aiosmtpd.controller import Controller
import smtplib
from email import message_from_bytes
from email.policy import default
class MyHandler:
async def handle_DATA(self, server, session, envelope):
peer = session.peer
mailfrom = envelope.mail_from
rcpttos = envelope.rcpt_tos
message = message_from_bytes(envelope.content, policy=default)
#HERE MAYBE WOULD BE SAFER TO WALK CONTENTS AND PARSE/MODIFY ONLY MAIL BODY, BUT NO SIDE EFFECTS UNTIL NOW WITH MIME, ATTACHMENTS...
messagetostring = message.as_string() ### smtplib.sendmail WANTED BYTES or STRING, NOT email OBJECT.
### HERE HAPPENS TEXT PROCESSING, STRING SUBSTITUTIONS...
### THIS WAS MY CORE NEED, ASYNCWAIT ON EACH THREAD
await asyncio.sleep(15)
server = smtplib.SMTP('localhost', 10027)
server.send_message(mailfrom, rcpttos, messagetostring) ### NEEDED TO INVERT ARGS ORDER
server.quit()
return '250 OK' ### ADDED RETURN
my_handler = MyHandler()
async def main(loop):
my_controller = Controller(my_handler, hostname='127.0.0.1', port=10025)
my_controller.start()
loop = asyncio.get_event_loop()
loop.create_task(main(loop=loop))
try:
loop.run_forever()

how to sign request tokens?

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.

Send Email If Not Sent In Last 24 Hours

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.

Categories