Asked  7 Months ago    Answers:  5   Viewed   130 times

How can I send the HTML content in an email using Python? I can send simple text.



From Python v2.7.14 documentation - 18.1.11. email: Examples:

Here’s an example of how to create an HTML message with an alternative plain text version:

#! /usr/bin/python

import smtplib

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# me == my email address
# you == recipient's email address
me = ""
you = ""

# Create message container - the correct MIME type is multipart/alternative.
msg = MIMEMultipart('alternative')
msg['Subject'] = "Link"
msg['From'] = me
msg['To'] = you

# Create the body of the message (a plain-text and an HTML version).
text = "Hi!nHow are you?nHere is the link you wanted:n"
html = """
       How are you?<br>
       Here is the <a href="">link</a> you wanted.

# Record the MIME types of both parts - text/plain and text/html.
part1 = MIMEText(text, 'plain')
part2 = MIMEText(html, 'html')

# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.

# Send the message via local SMTP server.
s = smtplib.SMTP('localhost')
# sendmail function takes 3 arguments: sender's address, recipient's address
# and message to send - here it is sent as one string.
s.sendmail(me, you, msg.as_string())
Tuesday, June 1, 2021
answered 7 Months ago

The BCC addresses are not stripped off at the destination email server. That's not how it works.

How SMTP actually works

  • The sender will send a list of RCPT TO commands to the SMTP server, one for each receiver email addresses, and this command does not distinguish whether the receiver is a normal To, CC or BCC type receiver.
  • Soon enough after calling the command that tells the SMTP server who's the sender, who's the server, and everything else, only then the sender will call the DATA command, in which will contain the content of the email - which consist of the email headers and body - the one that are received by email clients. Among these email headers are the usual from address, to address, CC address.
  • The BCC address is not shown to the receiver, simply because it's not printed out under the DATA command, not because the destination SMTP server stripped them away. The destination SMTP server will just refer to the RCPT TO for the list of email addresses that should receive the email content. It does not really care whether the receiver is in the To, CC or BCC list.
    Update (to clarify): BCC email addresses must be listed in the RCPT TO command list, but the BCC header should not be printed under the DATA command.

Quoting a part of the RFC that I think is relevant to your case:

Please note that the mail data includes the memo header items such as Date, Subject, To, Cc, From [2].

Rolling out your own email sender

A couple of years ago, I frankly think, is quite a long time back to assume that you still memorize end-to-end of RFC 821. :)

Wednesday, March 31, 2021
answered 9 Months ago

You can loop it 200 times with few problems I would imagine, although it will be much slower than a custom mailer or a package set up properly to handle that.

The end result depends on many factors. The main thing you'll want to make sure of is that you use set_time_limit() to give the script enough time to do the work. Offloading the work into some kind of queue that's serviced by a cron script can make life easier on you as well, as keeping PHP scripts running for a long time will bring up other resource problems.

Back in the day, I used to send about 50,000 emails to a subscriber newsletter using PHP's mail function and a RedHat server with Exim installed. It would take 4-6 hours with the custom script I had running. There was nothing efficient about it, but it did the job.

Saturday, May 29, 2021
answered 7 Months ago

This code sends the message in the typical plain text plus html multipart/alternative format. If your correspondent reads this in an html-aware mail reader, he's see the HTML table. If he reads it plain-text reader, he'll see the plain text version.

In either case, he will see the data included in the body of the message, and not as an attachment.

import csv
from tabulate import tabulate
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib

me = ''
password = 'yyyzzz!!2'
server = ''
you = ''

text = """
Hello, Friend.

Here is your data:




html = """
<html><body><p>Hello, Friend.</p>
<p>Here is your data:</p>

with open('input.csv') as input_file:
    reader = csv.reader(input_file)
    data = list(reader)

text = text.format(table=tabulate(data, headers="firstrow", tablefmt="grid"))
html = html.format(table=tabulate(data, headers="firstrow", tablefmt="html"))

message = MIMEMultipart(
    "alternative", None, [MIMEText(text), MIMEText(html,'html')])

message['Subject'] = "Your data"
message['From'] = me
message['To'] = you
server = smtplib.SMTP(server)
server.login(me, password)
server.sendmail(me, you, message.as_string())
Wednesday, July 14, 2021
answered 5 Months ago

Well, it looks like the problem was much simpler than I had thought. You have to "rewind" StringIO instances after you're completely done writing to them:

def spider_closed(self, spider):
    files = []

    for name, contents in self.files.items():

        files.append((name, 'text/plain', contents))

    return self.mail.send(
        subject="Crawler for %s finished" %,

For anyone that's interested, here's my email extension:

import gzip
import datetime

from scrapy import signals
from scrapy.mail import MailSender
from scrapy.exceptions import NotConfigured
from scrapy.utils.serialize import ScrapyJSONEncoder

from collections import defaultdict

    from cStringIO import cStringIO as StringIO
except ImportError:
    from StringIO import StringIO

def format_size(size):
    for x in ['bytes', 'KB', 'MB', 'GB']:
        if size < 1024.0:
            return "%3.1f %s" % (size, x)

        size /= 1024.0

class GzipCompressor(gzip.GzipFile):
    extension = '.gz'
    mimetype = 'application/gzip'

    def __init__(self):
        super(GzipCompressor, self).__init__(fileobj=PlainCompressor(), mode='w') =

class PlainCompressor(StringIO):
    extension = ''
    mimetype = 'text/plain'

    def read(self, *args, **kwargs):

        return, *args, **kwargs)

    def size(self):
        return len(self.getvalue())

class StatusMailer(object):
    def __init__(self, recipients, mail, compressor, crawler):
        self.recipients = recipients
        self.mail = mail
        self.encoder = ScrapyJSONEncoder(crawler=crawler)
        self.files = defaultdict(compressor)

        self.num_items = 0
        self.num_errors = 0

    def from_crawler(cls, crawler):
        recipients = crawler.settings.getlist('STATUSMAILER_RECIPIENTS')
        compression = crawler.settings.get('STATUSMAILER_COMPRESSION')

        if not compression:
            compressor = PlainCompressor
        elif compression.lower().startswith('gz'):
            compressor = GzipCompressor
            raise NotConfigured

        if not recipients:
            raise NotConfigured

        mail = MailSender.from_settings(crawler.settings)
        instance = cls(recipients, mail, compressor, crawler)

        crawler.signals.connect(instance.item_scraped, signal=signals.item_scraped)
        crawler.signals.connect(instance.spider_error, signal=signals.spider_error)
        crawler.signals.connect(instance.spider_closed, signal=signals.spider_closed)
        crawler.signals.connect(instance.request_received, signal=signals.request_received)

        return instance

    def item_scraped(self, item, response, spider):
        self.files[ + '-items.json'].write(self.encoder.encode(item))
        self.num_items += 1

    def spider_error(self, failure, response, spider):
        self.files[ + '.log'].write(failure.getTraceback())
        self.num_errors += 1

    def request_received(self, request, spider):
        self.files[ + '.log'].write(str(request) + 'n')

    def spider_closed(self, spider, reason):
        files = []

        for name, compressed in self.files.items():
            files.append((name + compressed.extension, compressed.mimetype, compressed))

            size = self.files[ + '-items.json'].size
        except KeyError:
            size = 0

        body='''Crawl statistics:

 - Spider name: {0}
 - Spider finished at: {1}
 - Number of items scraped: {2}
 - Number of errors: {3}
 - Size of scraped items: {4}'''.format(

        return self.mail.send(
            subject='Crawler for %s: %s' % (, reason),

Add it to your

    'your_package.extensions.StatusMailer': 80

And configure it:


Friday, October 1, 2021
answered 2 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :