# Author: Zhang Huangbin <zhb@iredmail.org>

import socket
import web
import settings
from libs import iredutils
from libs.amavisd import QUARANTINE_TYPES

session = web.config.get("_session")

# Import backend related modules.
if settings.backend == "ldap":
    from libs.ldaplib.admin import get_managed_domains
elif settings.backend in ["mysql", "pgsql"]:
    from libs.sqllib.admin import get_managed_domains


def get_raw_message(mail_id: str) -> bytes:
    """Get raw mail message of quarantined email specified by `mail_id`."""
    # TODO Check domain access by sender/recipient of quarantined email
    if not mail_id:
        return False, "INVALID_MAILID"

    try:
        records = web.conn_amavisd.select(
            "quarantine",
            vars={"mail_id": mail_id},
            what="mail_text",
            where="mail_id=$mail_id",
            order="chunk_ind ASC",
        )

        if not records:
            return False, "INVALID_MAILID"

        # Combine mail_text as RAW mail message.
        # Note: `mail_text` is bytes type.
        message = b""
        records = list(records)
        for i in records:
            message += i['mail_text']

        return True, message
    except Exception as e:
        return False, repr(e)


# If msgs.quar_type != "Q" (SQL), we can't get mail body.
def get_quarantined_mails(page=1,
                          account_type=None,
                          account="",
                          quarantined_type="",
                          size_limit=settings.PAGE_SIZE_LIMIT,
                          sort_by_score=False):
    """Return ([True | False], (total, records))"""

    page = int(page)
    account = str(account) or None

    # Pre-defined values.
    count = 0
    records = []
    sql_append_selection = ''

    # Domain names under control.
    all_domains = []

    # Query SQL.
    if session.get('is_normal_admin'):
        # List all managed domains in query if admin is not global admin
        qr = get_managed_domains(admin=session.get('username'), domain_name_only=True)
        if qr[0]:
            all_domains = qr[1]

        all_reversed_domains = iredutils.reverse_amavisd_domain_names(all_domains)

        if all_domains:
            sql_append_selection += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
                web.sqlquote(all_reversed_domains),
                web.sqlquote(all_reversed_domains),
            )
        else:
            return True, (0, {})

    if account_type == 'domain':
        if account:
            reversed_account = iredutils.reverse_amavisd_domain_names([account])[0]

            if not session.get('is_global_admin'):
                # Make sure account is managed domain
                if account not in all_domains:
                    # PERMISSION_DENIED
                    return True, (0, {})

            sql_append_selection += ' AND (sender.domain={} OR recip.domain={})'.format(
                web.sqlquote(reversed_account), web.sqlquote(reversed_account),
            )
    elif account_type == 'user':
        if session.get('is_normal_admin'):
            # Make sure account is under managed domains
            if account.split('@', 1)[-1] not in all_domains:
                # PERMISSION_DENIED
                return True, (0, {})
        elif session.get('account_is_mail_user'):
            if account != session['username']:
                return True, (0, {})

        sql_append_selection += ' AND (sender.email={} OR recip.email={})'.format(
            web.sqlquote(account),
            web.sqlquote(account),
        )

    if quarantined_type == 'spam':
        sql_append_selection += " AND msgs.content IN ('S', 's', 'Y')"
    elif quarantined_type == 'virus':
        sql_append_selection += " AND msgs.content = 'V'"
    elif quarantined_type == 'banned':
        sql_append_selection += " AND msgs.content = 'B'"
    elif quarantined_type == 'badheader':
        sql_append_selection += " AND msgs.content = 'H'"
    elif quarantined_type == 'badmime':
        sql_append_selection += " AND msgs.content = 'M'"

    # Get number of total records. SQL table: amavisd.msgs
    try:
        # Refer to templates/default/macros/amavisd.html for more detail
        # about msgs.content (content type, spam status), msgs.quar_type
        # (quarantine type).
        result = web.conn_amavisd.query(
            """
            -- Get number of quarantined emails
            SELECT COUNT(msgs.mail_id) AS total
            FROM msgs
            LEFT JOIN msgrcpt ON msgs.mail_id = msgrcpt.mail_id
            LEFT JOIN maddr AS sender ON msgs.sid = sender.id
            LEFT JOIN maddr AS recip ON msgrcpt.rid = recip.id
            WHERE
                -- msgs.content IN ('S', 's', 'Y', 'V', 'B', 'H')
                -- AND msgs.quar_type = 'Q'
                msgs.quar_type = 'Q'
                %s
            """ % sql_append_selection)

        count = result[0].total or 0
    except:
        pass

    # Get records of quarantined mails.
    try:
        # msgs.content:
        #   - S: spam(kill)
        #   - s: prior to 2.7.0 the CC_SPAMMY was logged as 's', now 'Y' is used.
        # msgs.quar_type:
        #   - Q: sql
        #   - F: file
        sort_column = 'msgs.time_num'
        if sort_by_score:
            sort_column = 'msgs.spam_level'

        result = web.conn_amavisd.query(
            '''
            -- Get records of quarantined mails.
            SELECT
                msgs.mail_id, msgs.secret_id, msgs.subject, msgs.time_num,
                msgs.content, msgs.size, msgs.spam_level,
                sender.email AS sender_email,
                recip.email AS recipient
            FROM msgs
            LEFT JOIN msgrcpt ON msgs.mail_id = msgrcpt.mail_id
            LEFT JOIN maddr AS sender ON msgs.sid = sender.id
            LEFT JOIN maddr AS recip ON msgrcpt.rid = recip.id
            WHERE
                -- msgs.content IN ('S', 's', 'Y', 'V', 'B', 'H')
                -- AND msgs.quar_type = 'Q'
                msgs.quar_type = 'Q'
                %s
            ORDER BY %s DESC
            LIMIT %d
            OFFSET %d
            ''' % (sql_append_selection, sort_column, size_limit, (page - 1) * size_limit)
        )
        records = iredutils.bytes2str(result)
    except:
        pass

    return True, (count, records)


def delete_all_quarantined(quarantined_type=None):
    if quarantined_type in QUARANTINE_TYPES:
        _content = QUARANTINE_TYPES[quarantined_type]

        # Delete them from `msgs`.
        # Records in `quarantine` will be cleaned up by cron job
        try:
            web.conn_amavisd.delete(
                'msgs',
                vars={'quar_type': 'Q', 'content': _content},
                where='quar_type=$quar_type AND content=$content',
            )
            return True,
        except Exception as e:
            return False, repr(e)
    else:
        try:
            web.conn_amavisd.delete('quarantine', where='1=1')
            web.conn_amavisd.delete('msgs', where="""quar_type='Q'""")
            return True,
        except Exception as e:
            return False, repr(e)


def release_quarantined_mails(records=None):
    # Release quarantined mails.
    #
    # records = [
    #    {'mail_id': 'xxx',
    #     'secret_id': 'yyy',
    #     'requested_by': session.get('username'),
    #    },
    #    [],
    # ]
    #
    # Refer to amavisd doc 'README.protocol' for more detail:
    #   - Releasing a message from a quarantine

    if not records:
        return True,

    # TODO Check domain_access
    #   - Get managed domains.
    #   - Check whether mail_id in `records` are one of managed domains.
    #   - Get allowed mail_id list.

    # Pre-defined variables.
    released_mail_ids = []

    # Create socket.
    try:
        quar_server = settings.amavisd_db_host
        quar_port = int(settings.amavisd_quarantine_port)

        if settings.AMAVISD_QUARANTINE_HOST:
            quar_server = settings.AMAVISD_QUARANTINE_HOST

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((quar_server, quar_port))
    except Exception as e:
        return False, repr(e)

    # Generate commands from dict, used for socket communication.
    # Note: We need to update Amavisd SQL database after mail was released
    #       with success, so do NOT send all release requests in ONE socket
    #       command although it will get better performance (a little).
    for record in records:
        # Skip record without 'mail_id'.
        if 'mail_id' not in record:
            continue

        cmd_release = 'request=release\r\n'

        for k in record:
            if record[k] is not None and record[k] != '':
                cmd_release += '{}={}\r\n'.format(k, record[k])

        cmd_release += 'quar_type=Q\r\n\r\n'
        try:
            s.send(cmd_release.encode())

            # Must wait for Amavisd's response before deleting SQL record,
            # otherwise we may delete sql record BEFORE Amavisd releases
            # quarantined email.
            s.recv(1024)

            released_mail_ids += [record.get('mail_id', 'NOT-EXIST')]
        except Exception as e:
            return False, repr(e)

    # Close socket.
    try:
        s.close()
    except Exception as e:
        return False, repr(e)

    # Return if no record was released successfully.
    if len(released_mail_ids) == 0:
        return True,

    # Update Amavisd SQL database.
    try:
        #   - Update msgs.content to 'C' (Clean)
        #       UPDATE msgs \
        #       SET msgs.content = 'C' \
        #       WHERE msgs.mail_id IN ('xxx', 'yyy', ..)
        #
        #   - Delete records in 'quarantine':
        #       DELETE FROM quarantine \
        #       WHERE quarantine.partition_tag = msgs.partition_tag \
        #       AND quarantine.mail_id = msgs.mail_id
        #
        web.conn_amavisd.update(
            'msgs',
            where='mail_id IN ' + web.sqlquote(released_mail_ids),
            quar_type='',
            content='C',
        )

        web.conn_amavisd.delete(
            'quarantine',
            where='mail_id IN ' + web.sqlquote(released_mail_ids),
        )

        return True,
    except Exception as e:
        return False, repr(e)
