Django: CSV in email attachment including Unicode characters results in extra linebreak - python

I have a reporting feature on my site that send CSV attached file by email. I recently noticed that if one of the string included an accent character my attached CSV has extra line break. Strange thing is I don't see any of these extra linebreak if the string doesn't contain any accent.
Code looks a bit like this:
# -*- coding: utf8 -*-
import unicodecsv
from StringIO import StringIO
from django.core.mail import EmailMultiAlternatives
# Generating the CSV
csvfile = StringIO()
writer = unicodecsv.writer(csvfile, encoding='utf-8')
writer.writerow([u'Test', u'Linebreak è'])
writer.writerow([u'Another', u'line'])
# Email
msg = EmailMultiAlternatives(
'csv report',
'Here is your attached report',
'email#from.com',
'email#to.com'
)
msg.attach('your_report.csv', csvfile.getvalue(), 'text/csv')
msg.send()
Opening the file with VIM shows me something like that:
Test,Linebreak è^M
Another,line
In comparison if the CSV rows include :
writer.writerow([u'Test', u'Linebreak'])
writer.writerow([u'Another', u'line'])
The attached CSV will look like that:
Test,Linebreak
Another,line
The getvalue() seems to output the right EOL formater but something seems to happen once the file is attached. Did someone else noticed similar issue?
(Runing Django 1.6 on python 2.7)
Edit: I have found the root of my problem. Turns out I'm using sendgrid for sending my emails, and for some reason their system is adding extra linebreak on my CSV when this one contains an accent...

As per commenter's request, I'll add a solution that involves Python's stamdard SMTP library instead of SendGrid.
As with OP's code, we use CSV data that is unicode. When it's time to prepare the message, we explictly add the data as UTF-8-encoded text attachment, and construct the message object like so:
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Write CSV data ...
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = recipients
msg.preamble = subject + '\n'
# Message body
body = MIMEText(body, 'plain', 'utf-8')
# CSV data
csv = MIMEText(csvfile.getvalue(), 'csv', 'utf-8')
csv.add_header("Content-Disposition", "attachment",
filename=report_filename)
# Add body and attachment to message
msg.attach(body)
msg.attach(csv)
You can read more about MIMEText in the Python library documentation. I find that passing it unicode strings (as opposed to str/bytes) works as long as there is a properly delcared charset.
Also, I have to note that I'm not sure whether the the newline problem was simply solved by using MIMEText attachment, or because of encoding. It's possible that using MIMEText object as attachment in OP's code may solve the problem. I leave experimentation to you, though.

For those who use Sendgrid as an SMTP provider to send you emails and if you noticed a similar issue, I fixed my problem by not using SMTP but the Web API of Sendgrid (via https://github.com/elbuo8/sendgrid-django).
No more extra lines in my CSV reports now!

Related

Getting invalid zip error when I try to open emailed Zip file using Python

So first of all I'm not really sure if I'm going about attaching this zip folder the right way. I've kinda hacked together different things together, but I'm getting an error message below which I've linked towards the bottom of my code.
from email.message import EmailMessage
import shutil
import smtplib
import base64
message = EmailMessage()
message["From"] = "superdummy#idiot.com"
message["To"] = "superdummy#idiot.com"
message["Subject"] = "Testing Zip"
path = "Testing.zip"
with open(path, "rb") as f:
bytes = f.read()
encoded = base64.b64encode(bytes)
message.add_attachment(encoded, maintype='application/zip', subtype='octet-stream', filename="Testing.zip")
s = smtplib.SMTP(host='blah.blah.yum', port=99)
s.send_message(message)
When I receive the email and I try to open it up, I get the error message:
"Windows cannot open the folder, the compressed Zip folder is invalid"
Couldn't find similar error messages on Stackoverflow or other places. I'd really appreciate some direction. Thanks!

Extracting Text from Gmail eml file using Python

Good Morning,
I have downloaded my *.eml from my Gmail and wanted to extract the content of the email as text.
I used the following codes:
import email
from email import policy
from email.parser import BytesParser
filepath = 'Project\Data\Your GrabPay Wallet Statement for 15 Feb 2022.eml'
fp = open(filepath, 'rb')
msg = BytesParser(policy=policy.default).parse(fp)
text = msg.get_body(preferencelist=('plain')).get_content()
I am unable to extract the content of the email. The length of text is 0.
When I attempted to open the *.eml using Word/Outlook, I could see the content.
When I use a normal file handler to open it:
fhandle = open(filepath)
print(fhandle)
print(fhandle.read())
I get
<_io.TextIOWrapper name='Project\Data\Your GrabPay Wallet Statement
for 15 Feb 2022.eml' mode='r' encoding='cp1252'>
And the contents look something like the one below:
Content-Transfer-Encoding: base64
Content-Type: text/html; charset=UTF-8
PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBYSFRNTCAxLjAgVHJhbnNpdGlvbmFs
Ly9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMS9EVEQveGh0bWwxLXRyYW5zaXRpb25h
bC5kdGQiPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+CjxoZWFk
I might have underestimated the amount of codes needed to extract email body content from *eml to Python.
I do not have access to your email, but I've been able to extract text from an email that I downloaded myself as a .eml from google.
import email
with open('email.eml') as email_file:
email_message = email.message_from_file(email_file)
print(email_message.get_payload())
When working with files it is important to consider using context managers such as I did in my example because it ensures that files are properly cleaned up and file handles are closed when they are no longer needed.
I briefly read over https://docs.python.org/3/library/email.parser.html for additional information on how to achieve the intended goal.
I realised the email is in multipart. So there is a need to get to the specific part, and decode the email. While doing do, it returns a chunk of HTML codes. To strip off the HTML codes and get plain-text, I used html2text.
import email
from email import policy
from email.parser import BytesParser
import html2text
filepath = 'Project\Data\Your GrabPay Wallet Statement for 15 Feb 2022.eml'
with open(filepath) as email_file:
email_message = email.message_from_file(email_file)
if email_message.is_multipart():
for part in email_message.walk():
#print(part.is_multipart())
#print(part.get_content_type())
#print()
message = str(part.get_payload(decode=True))
plain_message = html2text.html2text(message)
print(plain_message)
print()

Send data as .csv email attachmet Python

I have a data, let's say
data = [
['header_1', 'header_2'],
['row_1_!', 'row_1_2'],
['row_2_1', 'row_2_2'],
]
I need to send that data as .csv file attachment to email message.
I can not save it as .csv and then attach existing csv - application is working in Googpe App Engine sandbox environment. so no files can be saved.
As I understand, email attachment consists of file name and file encoded as base64.
I tried to make attachment body in the following way:
import csv
if sys.version_info >= (3, 0):
from io import StringIO
else:
from StringIO import StringIO
in_memory_data = StringIO()
csv.writer(inmemory_data).writerows(data)
encoded = base64.b64encode(inmemory_data.getvalue())
But in result I have received by email not valid file 2 columns and 3 rows, but just one string in file (see the picture).
csv_screen
What I'm doing wrong?
I've found out the mistake. I should have been convert it to bytearray instead of encoding to base64:
encoded = bytearray(inmemory_data.getvalue(), "utf-8")
Worked fine that way.

Sending multiple HTML Tables in email

I have 3 HTML Tables from three .html files that I need to email using Python one below the other.
Currently only 1 table is getting attached. How to attach all 3?
#!/usr/bin/python
import time
import os,sys
from os import path
import re
import sys, ast
import subprocess
# Import smtplib for the actual sending function
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from subprocess import Popen, PIPE
msg = MIMEMultipart('alternative')
# Open a plain text file for reading. For this example, assume that
# the text file contains only ASCII characters.
html = open('/root/madhu_test/bpstest/results/outnss.html')
htmla = open('/root/madhu_test/bpstest/results/outs2c.html')
htmlb = open('/root/madhu_test/bpstest/results/outrecommended.html')
html = html.read()
htmla = htmla.read()
htmlb = htmlb.read()
part2 = MIMEText(html, 'html')
part3 = MIMEText(htmla, 'html')
part4 = MIMEText(htmlb, 'html')
msg.attach(part2)
msg.attach(part3)
msg.attach(part4)
msg["From"] = "sauravb#juniper.net"
msg["To"] = "sauravb#juniper.net"
msg["Subject"] = "Sanity performance report"
p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE)
p.communicate(msg.as_string())
I had the same requirement. I used a single variable called "report" that contains all the html code in it. So I keep adding any number of tables to report varaible
report = ""
report += table1data
report += "<br><br>"
report += table2data
.
.
Finally attach this variable as the email text.
This worked for me.
A better solution might be to change MIMEMultipart('alternative') to MIMEMultipart('mixed')
This way it appends each attachment as opposed to choosing the best match for the email client.

How to send a zip file as an attachment in python?

I have looked through many tutorials, as well as other question here on stack overflow, and the documentation and explanation are at minimum, just unexplained code. I would like to send a file that I already have zipped, and send it as an attachment. I have tried copy and pasting the code provided, but its not working, hence I cannot fix the problem.
So what I am asking is if anyone knows who to explain how smtplib as well as email and MIME libraries work together to send a file, more specifically, how to do it with a zip file. Any help would be appreciated.
This is the code that everyone refers to:
import smtplib
import zipfile
import tempfile
from email import encoders
from email.message import Message
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
def send_file_zipped(the_file, recipients, sender='you#you.com'):
myzip = zipfile.ZipFile('file.zip', 'w')
# Create the message
themsg = MIMEMultipart()
themsg['Subject'] = 'File %s' % the_file
themsg['To'] = ', '.join(recipients)
themsg['From'] = sender
themsg.preamble = 'I am not using a MIME-aware mail reader.\n'
msg = MIMEBase('application', 'zip')
msg.set_payload(zf.read())
encoders.encode_base64(msg)
msg.add_header('Content-Disposition', 'attachment',
filename=the_file + '.zip')
themsg.attach(msg)
themsg = themsg.as_string()
# send the message
smtp = smtplib.SMTP()
smtp.connect()
smtp.sendmail(sender, recipients, themsg)
smtp.close()
I suspect the issue is this code zips a file as well. I don't want to zip anything as I already have a zipped file I would like to send. In either case, this code is poorly documented as well as the python libraries themselves as they provide no insight on anything past img file and text files.
UPDATE: Error I am getting now. I have also updated what is in my file with the code above
Traceback (most recent call last):
File "/Users/Zeroe/Documents/python_hw/cgi-bin/zip_it.py", line 100, in <module>
send_file_zipped('hw5.zip', 'avaldez#oswego.edu')
File "/Users/Zeroe/Documents/python_hw/cgi-bin/zip_it.py", line 32, in send_file_zipped
msg.set_payload(myzip.read())
TypeError: read() takes at least 2 arguments (1 given)
I don't really see the problem. Just omit the part which creates the zip file and, instead, just load the zip file you have.
Essentially, this part here
msg = MIMEBase('application', 'zip')
msg.set_payload(zf.read())
encoders.encode_base64(msg)
msg.add_header('Content-Disposition', 'attachment',
filename=the_file + '.zip')
themsg.attach(msg)
creates the attachment. The
msg.set_payload(zf.read())
sets, well, the payload of the attachment to what you read from the file zf (probably meaning zip file).
Just open your zip file beforehand and let this line read from it.
I agree the email package is not well documented yet. I investigated it before and wrote a wrapper module that simplifies these kinds of tasks. For example, the following works:
from pycopia import ezmail
# Get the data
data = open("/usr/lib64/python2.7/test/zipdir.zip").read()
# Make a proper mime message object.
zipattachement = ezmail.MIMEApplication.MIMEApplication(data, "zip",
filename="zipdir.zip")
# send it.
ezmail.ezmail(["Here is the zip file.", zipattachement],
To="me#mydomain.com", From="me#mydomain.com", subject="zip send test")
And that's all you need once you have everything installed and configured. :-)
My answer uses shutil to zip a directory containing the attachments and then adds the .zip to the email.
# Importing Dependencies
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import smtplib
import shutil
def Send_Email():
# Create a multipart message
msg = MIMEMultipart()
Body = MIMEText( {Enter Email Body as str here} )
# Add Headers
msg['Subject'] = ''
msg['From'] = ''
msg['To'] = ''
msg['CC'] = ''
msg['BCC'] = ''
# Add body to email
msg.attach(Body)
# Using Shutil to Zip a Directory
dir_name = {Add Path of the Directory to be Zipped}
output_filename = {Add Output Zip File Path}
shutil.make_archive(output_filename, 'zip', dir_name)
part = MIMEBase("application", "octet-stream")
part.set_payload(open(output_filename + ".zip", "rb").read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", "attachment; filename=\"%s.zip\"" % (output_filename))
msg.attach(part)

Categories