#! /usr/bin/env python
# -*- coding: utf-8 -*-

"""DESCRIPTION
        Ce script repère les comptes inactifs en parsant les logs de dernière
        connexion de sshd et dovecot, et en lisant et mettant à jour les champs
        derniereConnexion dans la base LDAP.

UTILISATION
        %(prog)s [action...]

ACTIONS POSSIBLES
%(acts)s"""

# Copyright (C) 2006 Stéphane Glondu
# Licence : GPLv2


import sys
import os
import re
import time
import cPickle
from time import mktime, time, localtime, strptime, strftime
from socket import gethostname
from smtplib import SMTP

host = gethostname()
debug = None
mail_report = u'disconnect@crans.org'
mail_sender = u"Comptes inactifs <disconnect@crans.org>"
template_path = '/usr/scripts/surveillance/comptes_inactifs/comptes_inactifs.%d.txt'
actions = ('log', 'dump', 'summary', 'spam')

sys.path.append('/usr/scripts/gestion')
from affich_tools import tableau, cprint
from email_tools import send_email, parse_mail_template
from ldap_crans import crans_ldap
from config import ann_scol
db = crans_ldap()

import syslog
syslog.openlog('comptes_inactifs')


def nb_mails_non_lus(login):
    """
    Renvoie le nombre de mails non lus de login, ou None si impossible à
    déterminer.
    """
    try:
        maildir = '/var/mail/%s/new' % login
        if os.path.isdir(maildir):
            return len(os.listdir(maildir))
        else:
            return 0
    except:
        # arrive quand le script n'a pas les bons droits pour lire /var/mail
        return None


class ComptesInactifs(object):
    # liste d'expressions régulières qui seront testées sur les lignes de log
    # le premier groupe doit correspondre à la date, le second au login
    compiled_regex = [re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}).*(?:'
                      r'dovecot.*Login: user=<|'
                      r'sshd.*Accepted.*for '
                      r')([^ >]+).*$'),
          re.compile(r'^.*comptes_inactifs.*derniereConnexion=<([^>]+)>, '
                      r'login=<([^>]+)>')]

    def __init__(self):
        """ Initialisation """
        self.dic = {}

    def search(self, query, mode=''):
        """ CransLdap.search allégé """
        query = '(&(objectClass=cransAccount)(objectClass=adherent)(%s))' % query
        result = db.conn.search_s(db.base_dn, db.scope['adherent'], query)
        result = [db.make(x, mode) for x in result]
        return result

    def commit_to_ldap(self):
        """
        Sauvegarde du dico dans la base LDAP.
        Renvoie le nombre d'entrées mises à jour.
        """
        total = 0
        for (login, timestamp) in self.dic.items():
            a = self.search('uid=%s' % login, 'w')
            if not a:
                # Probablement un adhérent récemment parti
                continue
            a = a[0]
            if a._modifiable == 'w':
                a.derniereConnexion(timestamp)
                if a.modifs: total += 1
                a.save()
            else:
                # on loggue on espérant que les logs seront réinjectés
                # plus tard
                syslog.syslog("LDAP(lock): derniereConnexion=<%s>, login=<%s>" %
                       (strftime("%Y-%m-%dT%H:%M:%S", localtime(timestamp)), login))
        return total

    def update(self, login, timestamp):
        """
        Met à jour l'entrée correspondant au login donné.
        """
        dic = self.dic
        timestamp = int(timestamp)
        if not dic.has_key(login) or timestamp > dic[login]:
            dic[login] = timestamp

    def update_from_syslog(self, loglines):
        """
        Met à jour le dico avec les lignes de syslog données.
        Renvoie le nombre de lignes traitées.
        """
        annee = localtime(time())[0]
        now = time() + 600
        nombre = 0
        for line in loglines:
            for r in self.compiled_regex:
                m = r.match(line)
                if m: break
            if not m: continue
            date = list(strptime(m.group(1), "%Y-%m-%dT%H:%M:%S"))
            t = mktime(date)
            self.update(m.group(2).lower(), t)
            nombre += 1
        return nombre

    def do_log(self):
        """
        Lit des lignes de log sur l'entrée std et met à jour la base LDAP.
        """
        parsed_lines = self.update_from_syslog(sys.stdin)
        updated_entries = self.commit_to_ldap()
        syslog.syslog("%(parsed_lines)s ligne(s) traitée(s)" % locals())
        syslog.syslog("%(updated_entries)s entrée(s) mise(s) à jour dans la base LDAP" % locals())
        if parsed_lines == 0 or updated_entries == 0:
            sys.stderr.write("""Erreur lors de la mise à jour de la base LDAP :
%(parsed_lines)s ligne(s) traitée(s)
%(updated_entries)s entrée(s) mise(s) à jour dans la base LDAP
""" % locals())

    def do_dump(self):
        """
        Affiche la liste des dernières connexions, triées par date.
        """
        liste = self.search('derniereConnexion=*')
        liste = [(x.derniereConnexion(), x.compte()) for x in liste]
        liste.sort()
        liste = [(x[1], strftime('%d/%m/%Y %H:%M', localtime(x[0])))
                 for x in liste]
        cprint(tableau(liste,
                       titre = (u'Login', u'Dernière connexion'),
                       largeur = (20, 20)))
        cprint(u"Total : %d" % len(liste))

    def get_idle_accounts(self, since=32*24*3600):
        """
        Renvoie la liste des objets Adherent de ceux qui ne se sont pas
        connectés depuis since secondes, par défaut un mois (32 jours,
        pour être sûr).
        """
        limit = int(time()) - since
        liste = self.search("derniereConnexion<=%d" % limit)
        for x in self.search("!(derniereConnexion=*)"):
            if x.dateInscription() <= limit:
                liste.append(x)
        return liste

    def do_summary(self):
        """
        Envoie à disconnect un résume des comptes inactifs depuis plus d'un
        mois.
        """
        modele = u"""*Membres inscrits ne s'étant pas connectés depuis plus d'un mois*

%(inscrits)s
Total : %(inscrits_total)d

*Anciens membres ne s'étant pas connectés depuis plus d'un mois*

%(anciens)s
Total : %(anciens_total)d

Légende :
 - F : existence d'un .forward
 - M : existence de mails non lus

--
comptes_inactifs.py
"""
        inscrits = []
        anciens = []

        liste = self.get_idle_accounts()
        # on trie par login
        liste.sort(lambda x, y: cmp(x.compte(), y.compte()))

        for a in liste:
            login = a.compte()
            date = a.derniereConnexion()
            if date:
                date = strftime(u'%d/%m/%Y %H:%M', localtime(date))
            else:
                date = u'Jamais'
            forward = os.path.isfile(os.path.join(a.home(), '.forward')) and u'X' or u''
            mail = nb_mails_non_lus(login)
            mail = mail == None and u'?' or mail > 0 and u'X' or u' '
            ligne = (a.id(), login, a.Nom(), date, forward, mail)
            if ann_scol in a.paiement() or a.adhesion() > time():
                inscrits.append(ligne)
            else:
                anciens.append(ligne)

        titres = (u'aid', u'Login', u'Nom', u'Dernière connexion', u'F', u'M')
        largeurs = (6, 15, 20, 20, 1, 1)
        alignements = ('d', 'g', 'c', 'c', 'c', 'c')

        inscrits_total = len(inscrits)
        inscrits = tableau(inscrits, titres, largeurs, alignements)
        anciens_total = len(anciens)
        anciens = tableau(anciens, titres, largeurs, alignements)

        send_email(mail_sender,
                   mail_report,
                   u'Comptes inactifs',
                   modele % locals(),
                   debug = debug)

    def do_spam(self):
        """
        Envoie un mail explicatif aux possesseurs de compte inactif
        (doit être exécuté en tant que root).
        """
        # Nombre de personnes concernées, en expansant de droite à gauche :
        # inscrit/ancien, avec/sans .forward, avec/sans mail non lu
        # Voir aussi template_path
        stats = [0, 0, 0, 0, 0, 0, 0, 0]

        # On factorise la connexion
        smtp = SMTP()
        smtp.connect()

        for a in self.get_idle_accounts():
            # initialisation des champs
            login = a.compte()
            mail = nb_mails_non_lus(login)
            nom = a.Nom()
            date = a.derniereConnexion() or a.dateInscription()
            date = strftime(u'%d/%m/%Y %H:%M', localtime(date))
            i = 0
            # est-ce un membre inscrit ?
            if not a.paiement_ok(): i += 4
            # a-t-il un .forward ?
            if not os.path.isfile(os.path.join(a.home(), '.forward')): i += 2
            # a-il-des mails non lus ?
            if not mail: i += 1
            # on incrémente
            stats[i] += 1
            if i == 1:
                # on laisse tranquilles les membres inscrits sans mails non
                # lus qui ont un .forward
                continue
            (sujet, corps) = parse_mail_template(template_path % i)
            corps = corps % locals()
            if debug:
                sujet = u"[Message de test %d] %s" % (i, sujet)
                if stats[i] > 1: continue

            send_email(mail_sender,
                       u"%s <%s@crans.org>" % (nom, login),
                       sujet,
                       corps,
                       server = smtp,
                       cc = debug,
                       debug = debug)

        recapitulatif = []
        total = 0
        for i in range(0, 8):
            total += stats[i]
            recapitulatif.append((((i & 4) and 'n' or 'o'),
                                  ((i & 2) and 'n' or 'o'),
                                  ((i & 1) and 'n' or 'o'),
                                  stats[i]))

        recapitulatif = tableau(recapitulatif,
                                titre = (u"Inscrits", u"Forward", u"Mails", u"Nombre"),
                                largeur = (10, 9, 7, 8),
                                alignement = ('c', 'c', 'c', 'd'))
        recapitulatif += u"""
Total : %d

--
comptes_inactifs.py
""" % total
        send_email(mail_sender,
                   mail_report,
                   u"Récapitulatif des comptes inactifs",
                   recapitulatif,
                   server = smtp,
                   debug = debug)

        smtp.quit()


def usage():
    """ Afficher l'aide. """
    prog = sys.argv[0]
    acts = [x + '\n' + '\n'.join(getattr(ComptesInactifs, 'do_' + x).__doc__.split('\n')[1:-1])
            for x in actions]
    acts = '\n'.join(acts)
    print __doc__ % locals()


if __name__ == '__main__':
    args = sys.argv[1:]

    if len(args) == 0:
        usage()
        sys.exit(0)

    for commande in args:
        if commande not in actions:
            sys.stderr.write("Commande incorrecte : %s\n" % commande)
            usage()
            sys.exit(2)

    ci = ComptesInactifs()
    for commande in args:
        getattr(ci, 'do_' + commande)()
