import datetime
import re
import smtplib
import time

from subprocess import Popen, PIPE

from email.utils import make_msgid, formatdate


# This is not a proper e-mail validator; but it will prevent us from
# accepting addresses which might break things. FIXME?
WEAK_EMAIL_RE = re.compile(r'^\S+@[A-Za-z0-9][A-Za-z0-9-\.]*[A-Za-z0-9]$')


def validate_email_identity(mailto):
    email = mailto.split(':', 1)[-1]
    if not mailto.startswith('mailto:'):
        raise ValueError('Not a mailto: identity')
    if '@' not in email:
        raise ValueError('E-mail addresses must have an @ sign')
    if not WEAK_EMAIL_RE.match(email):
        raise ValueError('That does not look like an e-mail address')
    return mailto


def make_email_hint(email):
    userpart, domain = email.split('@', 1)
    userpart = userpart.split(':')[-1]
    u1 = userpart[:max(1, len(userpart)//3)]
    if domain in ('gmail.com', 'hotmail.com'):
       # These domains have so many users they don't need to be anonymized,
       # and helping the user recognize them feels like a usability win.
       d1, d2 = '', domain
    else:
       d1 = domain[0]
       d2 = domain[-(2*(len(domain)-1))//3:]
    return '%s*@%s*%s' % (u1, d1, d2)


class MailtoHandler:
    MESSAGE_SUBJECT = "Passcrow Verification Code"
    MESSAGE_BODY = """\
Your verification code is: %(vcode)s

This is in regards to recovery of: "%(description)s"

If you were not expecting this verification code, please ignore it.
"""
    OUTER_MESSAGE_TEMPLATE = """\
To: %(to)s
From: %(from)s
Subject: %(subject)s
Date: %(date)s
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
User-Agent: Passcrow Server

%(message)s

-- 
Generated by Passcrow, an open community system for password recovery.
Read more here: %(about_url)s
"""

    def __init__(self,
            smtp_server=None, 
            smtp_login=None,
            smtp_password=None,
            sendmail_binary=None,
            outer_template=None,
            mail_from=None):

        if sendmail_binary and smtp_server:
            raise ValueError(
                'Please provide a sendmail binary or SMTP server, not both')

        if not sendmail_binary and not smtp_server:
            sendmail_binary = '/usr/sbin/sendmail'

        if not mail_from:
            if sendmail_binary:
                import socket, os, pwd
                mail_from = ('Passcrow <%s@%s>' % (
                   os.getenv('USER', pwd.getpwuid(os.getuid()).pw_name),
                   os.getenv('HOSTNAME', socket.gethostname())))

        self.smtp_server = smtp_server
        self.smtp_login = smtp_login
        self.smtp_password = smtp_password
        self.sendmail_binary = sendmail_binary
        self.mail_from = mail_from
        self.outer_template = outer_template or self.OUTER_MESSAGE_TEMPLATE

    def subject(self, server, description, vcode):
        return self.MESSAGE_SUBJECT % {
            'description': description,
            'vcode': vcode}

    def body(self, server, description, vcode, tmo_seconds):        
        return self.MESSAGE_BODY % {
            'description': description,
            'vcode': vcode,
            'timeout_seconds': tmo_seconds,
            'timeout_minutes': (tmo_seconds // 60)}

    def _send_via_sendmail(self, server, email, message):
        message = bytes(message, 'utf-8')

        proc = Popen([self.sendmail_binary, email],
            stdin=PIPE, stdout=PIPE, stderr=PIPE)
        (so, se) = proc.communicate(input=message)

        output = str(so + se, 'utf-8')
        server.log('sendmail(%d bytes to %s)=>%d %s'
            % (len(message), email, proc.returncode, output or '(no output)'))

        if proc.returncode != 0:
            raise IOError(se) 

    def _send_via_smtp(self, server, email, message):
        message = bytes(message, 'utf-8')
        pmap = {
            25: smtplib.SMTP,
            587: smtplib.SMTP,
            465: smtplib.SMTP_SSL}

        if ':' in self.smtp_server:
            host, port = self.smtp_server.split(':')
            ports = [int(port)]
        else:
            host, ports = self.smtp_server, [465, 587, 25]

        err = 'Failed?' 
        for tries, port in enumerate(ports + ports):
            try:
                smtp_cls = pmap.get(port, smtplib.SMTP)
                server.log('SMTP connect(%d) %s(%s, %d)'
                    % (tries, smtp_cls.__name__, host, port))
                with smtp_cls(host, port) as srv:
                    if smtp_cls != smtplib.SMTP_SSL:
                        srv.starttls()
                    if self.smtp_login:
                        srv.login(self.smtp_login, self.smtp_password)
                    srv.sendmail(self.mail_from, email, message)
                    return True
            except smtplib.SMTPAuthenticationError:
                err = 'SMTP login failed'
                break
            except (smtplib.SMTPException, IOError) as e:
                if tries == len(ports)-1:
                    time.sleep(2)
                err = e

        raise IOError('Failed to send: %s' % err)

    def send_code(self, server, mailto, description, vcode, timeout_seconds):
        email = validate_email_identity(mailto).split(':', 1)[-1]
        message = self.outer_template % {
            'to': email,
            'from': self.mail_from,
            'subject': self.subject(server, description, vcode),
            'message': self.body(server, description, vcode, timeout_seconds),
            'about_url': server.about_url,
            'message-id': make_msgid(),
            'date': formatdate()}

        if self.sendmail_binary:
            self._send_via_sendmail(server, email, message)
        else:
            self._send_via_smtp(server, email, message)

        return make_email_hint(mailto)


from ..proto import register_identity_kind
register_identity_kind('mailto', validate_email_identity, 'e-mail to')
