I want to use the credentials of the logged-in Windows user to authenticate an SMTP connection to an Exchange server using NTLM.
I'm aware of the python-ntlm module and the two patches that enable NTLM authentication for SMTP, however I want to use the current user's security token and not have to supply a username and password.
Very similar problem to Windows Authentication with Python and urllib2.
Although the solution below only uses the Python Win32 extensions (the sspi example code included with the Python Win32 extensions was very helpful), the python-ntlm IMAP & SMTP patches mentioned in the question also served as useful guides.
from smtplib import SMTPException, SMTPAuthenticationError
import string
import base64
import sspi
# NTLM Guide -- http://curl.haxx.se/rfc/ntlm.html
SMTP_EHLO_OKAY = 250
SMTP_AUTH_CHALLENGE = 334
SMTP_AUTH_OKAY = 235
def asbase64(msg):
return string.replace(base64.encodestring(msg), '\n', '')
def connect_to_exchange_as_current_user(smtp):
"""Example:
>>> import smtplib
>>> smtp = smtplib.SMTP("my.smtp.server")
>>> connect_to_exchange_as_current_user(smtp)
"""
# Send the SMTP EHLO command
code, response = smtp.ehlo()
if code != SMTP_EHLO_OKAY:
raise SMTPException("Server did not respond as expected to EHLO command")
sspiclient = sspi.ClientAuth('NTLM')
# Generate the NTLM Type 1 message
sec_buffer=None
err, sec_buffer = sspiclient.authorize(sec_buffer)
ntlm_message = asbase64(sec_buffer[0].Buffer)
# Send the NTLM Type 1 message -- Authentication Request
code, response = smtp.docmd("AUTH", "NTLM " + ntlm_message)
# Verify the NTLM Type 2 response -- Challenge Message
if code != SMTP_AUTH_CHALLENGE:
raise SMTPException("Server did not respond as expected to NTLM negotiate message")
# Generate the NTLM Type 3 message
err, sec_buffer = sspiclient.authorize(base64.decodestring(response))
ntlm_message = asbase64(sec_buffer[0].Buffer)
# Send the NTLM Type 3 message -- Response Message
code, response = smtp.docmd("", ntlm_message)
if code != SMTP_AUTH_OKAY:
raise SMTPAuthenticationError(code, response)
Great answer but as an update for python 3.0+
def asbase64(msg):
# encoding the message then convert to string
return((base64.b64encode(msg)).decode("utf-8"))
Python 2.7.x will fail on sending the NTLM Type 3 message due to the blank cmd specified:
code, response = smtp.docmd("", ntlm_message)
This ends up sending the correct response back to the server, however it pre-pends a space due to the nature of docmd() calling putcmd().
smtplib.py:
def putcmd(self, cmd, args=""):
"""Send a command to the server."""
if args == "":
str = '%s%s' % (cmd, CRLF)
else:
str = '%s %s%s' % (cmd, args, CRLF)
self.send(str)
# ...
def docmd(self, cmd, args=""):
"""Send a command, and return its response code."""
self.putcmd(cmd, args)
return self.getreply()
which as a result takes the path of the else condition, thereby sending str(' ' + ntlm_message + CRLF)
which results in (501, 'Syntax error in parameters or arguments')
.
As such the fix is simply to send the NTLM message as the cmd.
code, response = smtp.docmd(ntlm_message)
A fix to the above answer was submitted, though who knows when it will be reviewed/accepted.