hide preview

What's next? verify your email address for reply notifications!

unverified 5y, 241d ago

Thank you for the guide, that was really helpful ! It took me one whole day to make it works ( fighting with server parameters, trying to understand the whole thing properly ), but at the end it is working : ).

Thanks

remark link
hide preview

What's next? verify your email address for reply notifications!

russell 5y, 240d ago

You are welcome. I'm glad this post helped.

hide preview

What's next? verify your email address for reply notifications!

unverified 5y, 235d ago

Please help in how way to call the function. I am getting error when i try to call the send_email()

remark link
hide preview

What's next? verify your email address for reply notifications!

russell 5y, 232d ago

Please paste your error message so I may try to trouble shoot.

hide preview

What's next? verify your email address for reply notifications!

65x0Ib7L 5y, 116d ago

Hello! How to get the Dkim key from a database field. For example, I have a object called from_domain, where it has a attribute called private key. So, Can I sign it in this way?

            privkey = "-----BEGIN RSA PRIVATE KEY-----\
                             %s\
            -----END RSA PRIVATE KEY----" % from_domain.private_key

            sig = dkim.sign(message=message.as_string(),
                        selector=str(from_domain.selector),
                        domain=from_domain.domain_name,
                        privkey=privkey,
                        include_headers=headers,
                        canonicalize=('relaxed','relaxed'),)

            message.add_header('DKIM-Signature', sig.lstrip("DKIM-Signature: "))
remark link
hide preview

What's next? verify your email address for reply notifications!

russell 5y, 115d ago

It seems like this idea could work, are you getting an error?

hide preview

What's next? verify your email address for reply notifications!

unverified 4y, 144d ago

If you have run into bytes / string trouble trying to run this script with Python 3 (tested with 3.6.8), please check the Stackoverflow answer here: https://stackoverflow.com/a/46984906/12334678

This helped me get my script working properly.

hide preview

What's next? verify your email address for reply notifications!

unverified 4y, 129d ago [edited]

maybe its update now you should do that

signature = dkim.sign(
        message=message.as_bytes(),
        selector=base64.b64encode(bytes(DKIM_SELECTOR, encoding='utf-8')),
        domain=base64.b64encode(bytes(DKIM_DOMAIN, encoding='utf-8')),
        privkey=bytes(DKIM_PRIVATE_KEY, encoding='utf-8'),
    )
hide preview

What's next? verify your email address for reply notifications!

unverified 3y, 68d ago

For Python 3 DKIM sign function must be like this:

sig = dkim.sign( message=msg.as_bytes(), selector=bytes(dkim_selector,'UTF8'), domain=bytes(sender_domain,'UTF8'), privkey=bytes(dkim_private_key,'UTF8'), include_headers=headers )

remark link
hide preview

What's next? verify your email address for reply notifications!

unverified 3y, 36d ago

Can confirm this change is needed for Python3, I also had to change the line msg["DKIM-Signature"] = sig.lstrip("DKIM-Signature: ") to msg["DKIM-Signature"] = sig[len("DKIM-Signature: "):].decode() The stackoverflow listed above showed the first part of that, but without the .decode() to change it from byte to string I kept getting the error AttributeError: 'bytes' object has no attribute 'encode' on the sendmail function.

remark link parent
hide preview

What's next? verify your email address for reply notifications!

TechnoSwiss 3y, 36d ago

And I just noticed that the stackexchange article actually includes that as sig = sig.decode() I just happened to miss that line when I was comparing.

remark link parent
hide preview

What's next? verify your email address for reply notifications!

russell 2y, 342d ago

Thanks for the feedback, I've recently upgraded the Remarkbox code base to Python 3 and I've updated the blog post with my new solution which should work for both Python 2 and Python 3.

hide preview

What's next? verify your email address for reply notifications!

unverified 3y, 20d ago

great doc!

hide preview

What's next? verify your email address for reply notifications!

unverified 2y, 248d ago

Thanks for this guide! I tried using it in conjunction with the Gmail API but couldn't figure out what was wrong and didn't want to investigate further. If someone has a similar problem, I ended up using sendgrid to care of DKIM & SPF. I found this to be a much easier solution.

hide preview

What's next? verify your email address for reply notifications!

unverified 2y, 132d ago

Are you able to provide an example of "dkim_selector" (DKIM selector), what should I be using for this?

remark link
hide preview

What's next? verify your email address for reply notifications!

russell 2y, 132d ago

Install the public key (.pub) as a DNS TXT record, where the record name ("selector") is 20180605._domainkey and the value body is:

v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcplYPRsqIFwXuggtH2XgQDMX+e+6sGnWdV8ld/FR9zgRAxB+DeiCEVooVvYt2JRZUEokgDFvys82Q+JTbN4qHNz19bdcBGrnTsnIFaQYpgeQYmPLdDtcWRKzTYMRNCnRmmEXyGv7WIDcaTapIq9NFgLmy1QT7ZTxuNjQtDB/2LwIDAQAB;

You may choose any selector, I happen to like to use YearMonthDay. Additionally you will substitute your public key in place of mine. Put each the line of the public key on a single line in the TXT record.

hide preview

What's next? verify your email address for reply notifications!

unverified 2y, 120d ago [edited]

Thanks a lot! I edited the script in this article and added below line:

send_email("I-Do-Exist-victim@gmail.com", "I-do-not-exist@attacker-domain.com", "Test Email Subject", "Some text msg", "", "localhost", "/etc/dkim/attacker-domain.com.20211126.pem", "20211126._domainkey")

addresses and domain are in reality different...I have purchased a domain and added a TXT record:

  • name: 20211126._domainkey
  • value: v=DKIM1; k=rsa; p=MyPublicKey;

I generated the public and private key as per this article steps (I do have a certificate for a site on this same attacker-domain.com but I think that's irrelevant, so didn't use keys from that cert).

however, i get the below response when checking /var/log/mail.log (I modified the addresses in the output):

Nov 30 10:50:00 Ubuntu postfix/postfix-script[2928]: starting the Postfix mail system
Nov 30 10:50:00 Ubuntu postfix/master[2930]: daemon started -- version 3.4.13, configuration /etc/postfix
Nov 30 10:50:11 Ubuntu postfix/postfix-script[3499]: fatal: the Postfix mail system is already running
Nov 30 11:11:05 Ubuntu postfix/smtpd[12030]: connect from localhost[127.0.0.1]
Nov 30 11:11:05 Ubuntu postfix/smtpd[12030]: AD0EA4205D: client=localhost[127.0.0.1]
Nov 30 11:11:05 Ubuntu postfix/cleanup[12034]: AD0EA4205D: message-id=<20211130111105.AD0EA4205D@Ubuntu.xxxI removed thisxxxx>
Nov 30 11:11:05 Ubuntu postfix/qmgr[2932]: AD0EA4205D: from=<I-do-not-exist@attacker-domain.com>, size=1369, nrcpt=1 (queue active)
Nov 30 10:50:00 Ubuntu postfix/postfix-script[2928]: starting the Postfix mail system
Nov 30 10:50:00 Ubuntu postfix/master[2930]: daemon started -- version 3.4.13, configuration /etc/postfix
Nov 30 10:50:11 Ubuntu postfix/postfix-script[3499]: fatal: the Postfix mail system is already running
Nov 30 11:11:05 Ubuntu postfix/smtpd[12030]: connect from localhost[127.0.0.1]
Nov 30 11:11:05 Ubuntu postfix/smtpd[12030]: AD0EA4205D: client=localhost[127.0.0.1]
Nov 30 11:11:05 Ubuntu postfix/cleanup[12034]: AD0EA4205D: message-id=<20211130111105.AD0EA4205D@Ubuntu.xxxI removed thisxxxx>
Nov 30 11:11:05 Ubuntu postfix/qmgr[2932]: AD0EA4205D: from=<I-do-not-exist@attacker-domain.com>, size=1369, nrcpt=1 (queue active)
Nov 30 11:11:05 Ubuntu postfix/smtpd[12030]: disconnect from localhost[127.0.0.1] ehlo=1 mail=1 rcpt=1 data=1 quit=1 commands=5
Nov 30 11:11:06 Ubuntu postfix/smtp[12035]: connect to gmail-smtp-in.l.google.com[2404:6800:4003:c03::1a]:25: Network is unreachable
Nov 30 11:11:09 Ubuntu postfix/smtp[12035]: AD0EA4205D: to=<I-Do-Exist-victim@gmail.com>, relay=gmail-smtp-in.l.google.com[74.125.24.27]:25, delay=3.3, delays=0.01/0.01/2.2/1.1, dsn=5.7.26, status=bounced (host gmail-smtp-in.l.google.com[74.125.24.27] said: 550-5.7.26 This message does not have authentication information or fails to 550-5.7.26 pass authentication checks. To best protect our users from spam, the 550-5.7.26 message has been blocked. Please visit 550-5.7.26  https://support.google.com/mail/answer/81126#authentication for more 550 5.7.26 information. s5si23402633plp.197 - gsmtp (in reply to end of DATA command))

Hence I edited the send_mail definition to below but still got the same error:

def send_email(
    to_email,
    sender_email,
    subject,
    message_text,
    message_html,
    relay,
    dkim_private_key_path,
    dkim_selector,
):
  1. What am I doing wrong?
  2. another question: the "from" can be substituted to be anything, as long as the domain exists? for example, I-do-not-exist@facebook.com? ...am I correct?
remark link
hide preview

What's next? verify your email address for reply notifications!

russell 2y, 119d ago

This log line tells you what gmail claims happened:

Nov 30 11:11:09 Ubuntu postfix/smtp[12035]: AD0EA4205D: to=<I-Do-Exist-victim@gmail.com>, relay=gmail-smtp-in.l.google.com[74.125.24.27]:25, delay=3.3, delays=0.01/0.01/2.2/1.1, dsn=5.7.26, status=bounced (host gmail-smtp-in.l.google.com[74.125.24.27] said: 550-5.7.26 This message does not have authentication information or fails to 550-5.7.26 pass authentication checks. To best protect our users from spam, the 550-5.7.26 message has been blocked. Please visit 550-5.7.26  https://support.google.com/mail/answer/81126#authentication for more 550 5.7.26 information. s5si23402633plp.197 - gsmtp (in reply to end of DATA command))

Are you sending through an SMTP server that is covered by the SPF record?

Reference:

remark link parent
hide preview

What's next? verify your email address for reply notifications!

unverified 2y, 117d ago

I didn't have SPF record, but I just added it as below but I still got the same error.

name: attacker-domain.com value: v=spf1 ipxxx.xxx.xxx.xxx +all

where xxx.xxx.xxx.xxx is the IP of the ubuntu machine that is executing the python script and Postfix.

you asked "Are you sending through an SMTP server..." so I guess I miss something, as all I did is installing Postfix (haven't touched its configuration other than choosing the attacker-domain.com when prompted to during installation), and bought a regular domain (didn't purchase anything related to emails) and set two TXT records for it (one for DKIM and one for SPF).

Isn't Postfix supposed to act as my SMTP server?

remark link parent
hide preview

What's next? verify your email address for reply notifications!

russell 2y, 116d ago

Postfix is a MTA which runs SMTP services, the default settings should be a closed relay only allowing outbound (from the python script). The SPF record should include an option to allow this server to send email on behalf of the domain.

hide preview

What's next? verify your email address for reply notifications!

unverified 1y, 287d ago

Thanks very much! This is a great tutorial, If anyone encounters this error:

"header_store_parse raise ValueError("Header values may not contain linefeed " from the email/policy.py on this line:

msg["DKIM-Signature"] = sig[len("DKIM-Signature:") :].decode()

you can replace the CR LF with:

sig = sig.replace(b"\r\n",b"") before assigning value to msg["DKIM-Signature"]

remark link
hide preview

What's next? verify your email address for reply notifications!

Yaroslaff 70d, 16h ago

This approach makes job twice:

  1. dkim.sign() wraps long header (with CR/LF)
  2. sig.replace() un-wraps it back

Better to add linesep=b'' argument to dkim.sign(), this will prevent problem. (I had same issue).

hide preview

What's next? verify your email address for reply notifications!

unverified 1y, 91d ago

This doesn't work any more. It used to, but something changed in one of the packages (I think the MIME encoder) and it messes up the header now.

remark link
hide preview

What's next? verify your email address for reply notifications!

unverified 135d, 12h ago

did you manage to figure it out?

hide preview

What's next? verify your email address for reply notifications!

Yaroslaff 70d, 16h ago [edited]

Thank you! I used this approach to add DKIM feature to my https://github.com/yaroslaff/testmsg project (CLI utility to make/send perfectly valid test messages).

When working on it, I got ValueError exception "Header values may not contain linefeed or carriage return characters".

I use message as instance of email.message.EmailMessage class and it does not like newlines in headers (it wrap headers itself). Solution was to add linesep=b'' to dkim.sign():

        sig = dkim.sign(
            message=msg.as_bytes(),
            selector=str(args.selector).encode(),
            domain=domainname.encode(),
            privkey=dkim_private_key.encode(),
            include_headers=headers,
            linesep=b''
        )

If someone wants to use testmsg sources as working example (and maybe borrow code) you are welcome!

hide preview

What's next? verify your email address for reply notifications!

strnbrg59 46d, 19m ago

Thanks for posting this! You're right; there's not much in the way of examples out there.

Unfortunately, it's failing for me:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/dkim/crypto.py", line 140, in parse_private_key
    pka = asn1_parse(ASN1_RSAPrivateKey, data)
  File "/usr/lib/python3/dist-packages/dkim/asn1.py", line 91, in asn1_parse
    raise ASN1FormatError(
dkim.asn1.ASN1FormatError: Unexpected tag (got 6f, expecting 30)

I'm using your code exactly, and calling your function like this:

if __name__ == '__main__':
    send_email(
        to_email='strnbrg59@gmail.com',
        sender_email='strnbrg59@strnbrg59.com',
        subject='dkim mail test',
        message_text='This is a test of dkim-signed mail',
        message_html='This is the html version',
        dkim_private_key_path='/home/strnbrg59/.ssh/id_rsa',
        dkim_selector='20240212._domainkey')

where 20240212._domainkey is the name I've assigned to the TXT record at godaddy.com.

Any thoughts? I am 100% sure my private/public key situation is good on this server.

hide preview

What's next? verify your email address for reply notifications!

strnbrg59 46d, 14m ago [edited]

Thanks for posting this! As you say, there's not much in the way of examples out there.

Unfortunately, it's not working for me:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/dkim/crypto.py", line 140, in parse_private_key
    pka = asn1_parse(ASN1_RSAPrivateKey, data)
  File "/usr/lib/python3/dist-packages/dkim/asn1.py", line 91, in asn1_parse
    raise ASN1FormatError(
dkim.asn1.ASN1FormatError: Unexpected tag (got 6f, expecting 30)

I'm using your code exactly, and calling it like so:

if __name__ == '__main__':
    send_email(
        to_email='strnbrg59@gmail.com',
        sender_email='strnbrg59@strnbrg59.com',
        subject='dkim mail test',
        message_text='This is a test of dkim-signed mail',
        message_html='This is the html version',
        dkim_private_key_path='/home/strnbrg59/.ssh/id_rsa',
        dkim_selector='20240212._domainkey')

where 20240212._domainkey is what I've named my DNS TXT record at godaddy.com. I'm 100% sure my private/public key situation on this server is good, and I've verified that I am supplying the correct path to my private key (which is a file that begins with "-----BEGIN OPENSSH PRIVATE KEY-----" and ends with "-----END OPENSSH PRIVATE KEY-----").

hide preview

What's next? verify your email address for reply notifications!