diff --git a/modules/webalert/bin/alertengine.in b/modules/webalert/bin/alertengine.in
index aee7d5bf5..b4bef0229 100644
--- a/modules/webalert/bin/alertengine.in
+++ b/modules/webalert/bin/alertengine.in
@@ -1,71 +1,82 @@
 #!@PYTHON@
 ## -*- mode: python; coding: utf-8; -*-
 ##
 ## $Id$
 ##
 ## This file is part of the CERN Document Server Software (CDSware).
 ## Copyright (C) 2002, 2003, 2004, 2005 CERN.
 ##
 ## The CDSware is free software; you can redistribute it and/or
 ## modify it under the terms of the GNU General Public License as
 ## published by the Free Software Foundation; either version 2 of the
 ## License, or (at your option) any later version.
 ##
 ## The CDSware is distributed in the hope that it will be useful, but
 ## WITHOUT ANY WARRANTY; without even the implied warranty of
 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 ## General Public License for more details.  
 ##
 ## You should have received a copy of the GNU General Public License
 ## along with CDSware; if not, write to the Free Software Foundation, Inc.,
 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 
 """Alert engine command line interface"""
 
 __version__ = "$Id$"
 
 try:
     import sys
     import getopt
     from cdsware.config import version, supportemail
     from cdsware.alert_engine import run_alerts
     from time import time
 except ImportError, e:
     print "Error: %s" % e
     import sys
     sys.exit(1)
 
-DEBUGLEVEL = 0
+import datetime
+
 
 def usage():
     print """Usage: alertengine [OPTION]
-Run the alert engine.\n
+Run the alert engine.
+
   -h, --help          display this help and exit
-  -V, --version       output version information and exit\n
+  -V, --version       output version information and exit
+
+  -d  --date="YEAR-MONTH-DAY" run the alertengine as if we were the
+                              specified day, for test purposes (today)
+
 Report bugs to <%s>""" % supportemail
         
 def main():
-    global DEBUGLEVEL
 
+    date = datetime.date.today()
+    
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "hV", ["help", "version"])
+        opts, args = getopt.getopt(sys.argv[1:], "hVd:",
+                                   ["help", "version", "date="])
     except getopt.GetoptError:
         usage()
         sys.exit(2)
 
     for o, a in opts:
         if o in ("-h", "--help"):
             usage()
             sys.exit()
         if o in ("-V", "--version"):
             print __version__
             sys.exit(0)
-
-    
-    run_alerts()
+        if o in ("-d", "--date"):
+            year, month, day = map(int, a.split('-'))
+            date = datetime.date(year, month, day)
+            
+        
+    run_alerts(date)
 
 if __name__ == "__main__":
     t0 = time()
     main()	
     t1 = time()
     print 'Alert engine finished in %.2f seconds' % (t1 - t0)
diff --git a/modules/webalert/lib/alert_engine.py b/modules/webalert/lib/alert_engine.py
index 114bdf09f..ac23b9e38 100644
--- a/modules/webalert/lib/alert_engine.py
+++ b/modules/webalert/lib/alert_engine.py
@@ -1,453 +1,408 @@
 ## $Id$
 ## Alert engine implementation.
 
 ## This file is part of the CERN Document Server Software (CDSware).
 ## Copyright (C) 2002, 2003, 2004, 2005 CERN.
 ##
 ## The CDSware is free software; you can redistribute it and/or
 ## modify it under the terms of the GNU General Public License as
 ## published by the Free Software Foundation; either version 2 of the
 ## License, or (at your option) any later version.
 ##
 ## The CDSware is distributed in the hope that it will be useful, but
 ## WITHOUT ANY WARRANTY; without even the implied warranty of
 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 ## General Public License for more details.  
 ##
 ## You should have received a copy of the GNU General Public License
 ## along with CDSware; if not, write to the Free Software Foundation, Inc.,
 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 
 """Alert engine implementation."""
 
 ## rest of the Python code goes below
 
 __version__ = "$Id$"
 
 from cgi import parse_qs
 from sre import search, sub
 from time import localtime, strftime, mktime, sleep
 from string import split
 import smtplib
+import datetime
 
 from email.Header import Header
 from email.Message import Message
 from email.MIMEText import MIMEText
 
 from cdsware.config import *
 from cdsware.search_engine import perform_request_search
 from cdsware.dbquery import run_sql
 from cdsware.htmlparser import *
 
-MAXIDS = 50
-FROMADDR = 'CDS Alert Engine <%s>' % alertengineemail
-ALERTURL = weburl + '/youralerts.py/list'
+import cdsware.template
+webalert_templates = cdsware.template.load('webalert')
+
+
 DEVELOPERADDR = [supportemail]
 
 # Debug levels:
 # 0 = production, nothing on the console, email sent
 # 1 = messages on the console, email sent
 # 2 = messages on the console, but no email sent
 # 3 = many messages on the console, no email sent
 # 4 = many messages on the console, email sent to DEVELOPERADDR
 DEBUGLEVEL = 0
 
 
 def update_date_lastrun(alert):
     return run_sql('update user_query_basket set date_lastrun=%s where id_user=%s and id_query=%s and id_basket=%s;', (strftime("%Y-%m-%d"), alert[0], alert[1], alert[2],))
 
 
 def get_alert_queries(frequency):
     return run_sql('select distinct id, urlargs from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.frequency=%s and uqb.date_lastrun <= now();', (frequency,))
     
 def get_alert_queries_for_user(uid):
     return run_sql('select distinct id, urlargs, uqb.frequency from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.id_user=%s and uqb.date_lastrun <= now();', (uid,))
 
 def get_alerts(query, frequency):
     r = run_sql('select id_user, id_query, id_basket, frequency, date_lastrun, alert_name, notification from user_query_basket where id_query=%s and frequency=%s;', (query['id_query'], frequency,))
     return {'alerts': r, 'records': query['records'], 'argstr': query['argstr'], 'date_from': query['date_from'], 'date_until': query['date_until']}
 
     
 # def add_record_to_basket(record_id, basket_id):
 #     if DEBUGLEVEL > 0:
 #         print "-> adding record %s into basket %s" % (record_id, basket_id)
 #     try:
 #         return run_sql('insert into basket_record (id_basket, id_record) values(%s, %s);', (basket_id, record_id,))
 #     except:
 #         return 0
 
     
 # def add_records_to_basket(record_ids, basket_id):
 #     # TBD: generate the list and all all records in one step (see below)
 #     for i in record_ids:
 #         add_record_to_basket(i, basket_id)
 
 # Optimized version:
 def add_records_to_basket(record_ids, basket_id):
-    global DEBUGLEVEL
   
     nrec = len(record_ids)
     if nrec > 0:
         vals = '(%s,%s)' % (basket_id, record_ids[0])
         if nrec > 1:
             for i in record_ids[1:]:
                 vals += ',(%s, %s)' % (basket_id, i)
         if DEBUGLEVEL > 0:
             print "-> adding %s records into basket %s: %s" % (nrec, basket_id, vals)
         try:
             if DEBUGLEVEL < 4:
                 return run_sql('insert into basket_record (id_basket, id_record) values %s;' % vals) # Cannot use the run_sql(<query>, (<arg>,)) form for some reason
             else:
                 print '   NOT ADDED, DEBUG LEVEL == 4'
                 return 0
         except:
             return 0
     else:
         return 0
     
 
 def get_email(uid):
     r = run_sql('select email from user where id=%s', (uid,))
     return r[0][0]
 
 def get_query(alert_id):
     r = run_sql('select urlargs from query where id=%s', (alert_id,))
     return r[0][0]
 
 def send_email(fromaddr, toaddr, body, attempt=0):
-    global DEBUGLEVEL
 
     if attempt > 2:
         log('error sending email to %s: SMTP error; gave up after 3 attempts' % toaddr)
         return
     
     try:
         server = smtplib.SMTP('localhost')
         if DEBUGLEVEL > 2:
             server.set_debuglevel(1)
         else:
             server.set_debuglevel(0)
             
         server.sendmail(fromaddr, toaddr, body)
         server.quit()
     except:
         if (DEBUGLEVEL > 1):
             print 'Error connecting to SMTP server, attempt %s retrying in 5 minutes. Exception raised: %s' % (attempt, sys.exc_info()[0])
         sleep(300)
         send_email(fromaddr, toaddr, body, attempt+1)
         return
 
 
 def forge_email(fromaddr, toaddr, subject, content):
     msg = MIMEText(content, _charset='utf-8')
     
-    msg ['From'] = fromaddr
-    msg ['To'] = toaddr
-    msg ['Subject'] = Header(subject, 'utf-8')
+    msg['From'] = fromaddr
+    msg['To'] = toaddr
+    msg['Subject'] = Header(subject, 'utf-8')
 
     return msg.as_string()
 
 
-def format_frequency(freq):
-    frequency = freq
-    if frequency == "day":
-        return 'daily'
-    else:
-        return frequency + 'ly'
-
-
-def print_records(record_ids):
-    global MAXIDS
-    msg = ''
-    c = 1
-    for i in record_ids:
-        if c > MAXIDS:
-            break
-        msg += '\n\n%s) %s' % (c, get_as_text(i))
-        c += 1
-
-    if c > MAXIDS:
-        msg += '\n\n' + wrap('Only the first %s records were displayed.  Please consult the search URL given at the top of this email to see all the results.' % MAXIDS)
-
-    return msg
-
-
-
 def email_notify(alert, records, argstr):
-    global FROMADDR
-    global ALERTURL
-    global DEBUGLEVEL
-    global DEVELOPERADDR
 
     if len(records) == 0:
         return
 
     msg = ""
     
     if DEBUGLEVEL > 0:
         msg = "*** THIS MESSAGE WAS SENT IN DEBUG MODE ***\n\n"
 
-    msg += "Hello\n\n"
-    msg += wrap("Below are the results of the email alert that you set up with the CERN Document Server.  This is an automatic message, please don't reply to its address.  For any question, use <%s> instead." % supportemail)
+    url = weburl + "/search.py?" + argstr
+
+    # Extract the pattern and catalogue list from the formatted query
+    query = parse_qs(argstr)
+    pattern = query.get('p', [''])[0]
+    catalogues = query.get('c', [])
+
+    frequency = alert[3]
+    
+    msg += webalert_templates.tmpl_alert_email_body(
+        alert[5], url, records, pattern, catalogues, frequency)
 
-    email = get_email(alert[0])
+    email = get_email(alert[0])    
         
-    url = weburl + "/search.py?" + argstr
-    pattern = get_pattern(argstr)
-    catalogue = get_catalogue(argstr)
-    catword = 'collection'
-    if get_catalogue_num(argstr) > 1:
-        catword += 's'
+    msg = MIMEText(msg, _charset='utf-8')
     
-    time = strftime("%Y-%m-%d")
-
-    msg += '\n' + wrap('alert name: %s' % alert[5])
-    if pattern:
-        msg += wrap('pattern: %s' % pattern)
-    if catalogue:
-        msg += wrap('%s: %s' % (catword, catalogue))
-    msg += wrap('frequency: %s ' % format_frequency(alert[3]))
-    msg += wrap('run time: %s ' % strftime("%a %Y-%m-%d %H:%M:%S"))
-    recword = 'record'
-    if len(records) > 1:
-        recword += 's'
-    msg += wrap('found: %s %s' % (len(records), recword))
-    msg += "url: <%s/search.py?%s>\n" % (weburl, argstr)
-
-    msg += wrap_records(print_records(records))
-
-    msg += "\n-- \nCERN Document Server Alert Service <%s>\nUnsubscribe?  See <%s>\nNeed human intervention?  Contact <%s>" % (weburl, ALERTURL, supportemail)
-
-    subject = 'Alert %s run on %s' % (alert[5], time)
+    msg['To'] = email
+
+    # Let the template fill in missing fields
+    webalert_templates.tmpl_alert_email_headers(alert[5], msg)
+
+    sender = msg['From']
     
-    body = forge_email(FROMADDR, email, subject, msg)
+    body = msg.as_string()
 
     if DEBUGLEVEL > 0:
         print "********************************************************************************"
         print body
         print "********************************************************************************"
 
     if DEBUGLEVEL < 2:
-        send_email(FROMADDR, email, body)
+        send_email(sender, email, body)
     if DEBUGLEVEL == 4:
         for a in DEVELOPERADDR:
-            send_email(FROMADDR, a, body)
+            send_email(sender, a, body)
 
 def get_argument(args, argname):
     if args.has_key(argname):
         return args[argname]
     else:
         return []
-    
+
+def _date_to_tuple(date):
+    return [str(part) for part in (date.year, date.month, date.day)]
+
 def get_record_ids(argstr, date_from, date_until):
     args = parse_qs(argstr)
     p       = get_argument(args, 'p')
     c       = get_argument(args, 'c')
     cc      = get_argument(args, 'cc')
     as      = get_argument(args, 'as')
     f       = get_argument(args, 'f')
     rg      = get_argument(args, 'rg')
     so      = get_argument(args, 'so')
     sp      = get_argument(args, 'sp')
     ot      = get_argument(args, 'ot')
     as      = get_argument(args, 'as')
     p1      = get_argument(args, 'p1')
     f1      = get_argument(args, 'f1')
     m1      = get_argument(args, 'm1')
     op1     = get_argument(args, 'op1')
     p2      = get_argument(args, 'p2')
     f2      = get_argument(args, 'f2')
     m2      = get_argument(args, 'm2')
     op2     = get_argument(args, 'op2')
     p3      = get_argument(args, 'p3')
     f3      = get_argument(args, 'f3')
     m3      = get_argument(args, 'm3')
     sc      = get_argument(args, 'sc')
     # search  = get_argument(args, 'search')
 
-    d1y, d1m, d1d = date_from
-    d2y, d2m, d2d = date_until
+    d1y, d1m, d1d = _date_to_tuple(date_from)
+    d2y, d2m, d2d = _date_to_tuple(date_until)
 
-    return perform_request_search(of='id', p=p, c=c, cc=cc, f=f, so=so, sp=sp, ot=ot, as=as, p1=p1, f1=f1, m1=m1, op1=op1, p2=p2, f2=f2, m2=m2, op2=op2, p3=p3, f3=f3, m3=m3, sc=sc, d1y=d1y, d1m=d1m, d1d=d1d, d2y=d2y, d2m=d2m, d2d=d2d)
+    return perform_request_search(of='id', p=p, c=c, cc=cc, f=f, so=so, sp=sp, ot=ot,
+                                  as=as, p1=p1, f1=f1, m1=m1, op1=op1, p2=p2, f2=f2,
+                                  m2=m2, op2=op2, p3=p3, f3=f3, m3=m3, sc=sc, d1y=d1y,
+                                  d1m=d1m, d1d=d1d, d2y=d2y, d2m=d2m, d2d=d2d)
 
 
 def get_argument_as_string(argstr, argname):
     args = parse_qs(argstr)
     a = get_argument(args, argname)
     r = ''
     if len(a):
         r = a[0]
     for i in a[1:len(a)]:
         r += ", %s" % i
     return r
     
 def get_pattern(argstr):
     return get_argument_as_string(argstr, 'p')
 
 def get_catalogue(argstr):
     return get_argument_as_string(argstr, 'c')
 
 def get_catalogue_num(argstr):
     args = parse_qs(argstr)
     a = get_argument(args, 'c')
     return len(a)
 
-def get_date_from(time, freq):
-    t = mktime(time)
-    if freq == 'day':
-        time2 = localtime(t - 86400)
-    elif freq == 'month':
-        m = time[1] - 1
-        y = time[0]
-        if m == 0:
-            m = 12
-            y -= 1
-        time2 = (y, m, time[2], time[3], time[4], time[5], time[6], time[7], time[8])
-    elif freq == 'week':
-        time2 = localtime(t - 604800)
-        
-    ystr = strftime("%Y", time2)
-    mstr = strftime("%m", time2)
-    dstr = strftime("%d", time2)
 
-    return (ystr, mstr, dstr)
-
-def run_query(query, frequency):
+def run_query(query, frequency, date_until):
     """Return a dictionary containing the information of the performed query.
 
     The information contains the id of the query, the arguments as a
     string, and the list of found records."""
 
-    time = localtime()
-    # Override time here for testing purposes (beware of localtime offset):
-    #time = (2002, 12, 21, 2, 0, 0, 2, 120, 1)
-    # Override frequency here for testing
-    #frequency = 'week'
-    ystr = strftime("%Y", time)
-    mstr = strftime("%m", time)
-    dstr = strftime("%d", time)
-    date_until = (ystr, mstr, dstr)
-    
-    date_from = get_date_from(time, frequency)
+    if frequency == 'day':
+        date_from = date_until - datetime.timedelta(days=1)
+
+    elif frequency == 'week':
+        date_from = date_until - datetime.timedelta(weeks=1)
+
+    else:
+        # Months are not an explicit notion of timedelta (it's the
+        # most ambiguous too). So we explicitely take the same day of
+        # the previous month.
+        d, m, y = (date_until.day, date_until.month, date_until.year)
+        m = m - 1
 
+        if m == 0:
+            m = 12
+            y = y - 1
+
+        date_from = datetime.date(year=y, month=m, day=d)
+    
     recs = get_record_ids(query[1], date_from, date_until)
 
     n = len(recs)
     if n:
         log('query %08s produced %08s records' % (query[0], len(recs)))
     
     if DEBUGLEVEL > 2:
-        print "[%s] run query: %s with dates: from=%s, until=%s\n  found rec ids: %s" % (strftime("%c"), query, date_from, date_until, recs)
+        print "[%s] run query: %s with dates: from=%s, until=%s\n  found rec ids: %s" % (
+            strftime("%c"), query, date_from, date_until, recs)
 
-    return {'id_query': query[0], 'argstr': query[1], 'records': recs, 'date_from': date_from, 'date_until': date_until}
+    return {'id_query': query[0], 'argstr': query[1],
+            'records': recs, 'date_from': date_from, 'date_until': date_until}
 
 
-def process_alert_queries(frequency):
+def process_alert_queries(frequency, date):
     """Run the alerts according to the frequency.
 
     Retrieves the queries for which an alert exists, performs it, and
     processes the corresponding alerts."""
     
     alert_queries = get_alert_queries(frequency)
 
     for aq in alert_queries:
-        q = run_query(aq, frequency)
+        q = run_query(aq, frequency, date)
         alerts = get_alerts(q, frequency)
         process_alerts(alerts)
 
 
 def replace_argument(argstr, argname, argval):
     """Replace the given date argument value with the new one.
 
     If the argument is missing, it is added."""
     
     if search('%s=\d+' % argname, argstr):
         r = sub('%s=\d+' % argname, '%s=%s' % (argname, argval), argstr)
     else:
         r = argstr + '&%s=%s' % (argname, argval)
         
     return r
     
 def update_arguments(argstr, date_from, date_until):
     """Replace date arguments in argstr with the ones specified by date_from and date_until.
 
     Absent arguments are added."""
     
-    d1y, d1m, d1d = date_from
-    d2y, d2m, d2d = date_until
+    d1y, d1m, d1d = _date_to_tuple(date_from)
+    d2y, d2m, d2d = _date_to_tuple(date_until)
 
     r = replace_argument(argstr, 'd1y', d1y)
     r = replace_argument(r, 'd1m', d1m)
     r = replace_argument(r, 'd1d', d1d)
     r = replace_argument(r, 'd2y', d2y)
     r = replace_argument(r, 'd2m', d2m)
     r = replace_argument(r, 'd2d', d2d)
         
     return r
 
 def log(msg):
     try:
         log = open(logdir + '/alertengine.log', 'a')
         log.write(strftime('%04Y%02m%02d%02H%02M%02S#'))
         log.write(msg + '\n')
         log.close()
     except:
         pass
     
 def process_alerts(alerts):
     # TBD: do not generate the email each time, forge it once and then
     # send it to all appropriate people
 
     for a in alerts['alerts']:
         if alert_use_basket_p(a):
             add_records_to_basket(alerts['records'], a[2])
         if alert_use_notification_p(a):
             argstr = update_arguments(alerts['argstr'], alerts['date_from'], alerts['date_until'])
             email_notify(a, alerts['records'], argstr)
             
         update_date_lastrun(a)
 
         
 def alert_use_basket_p(alert):
     return alert[2] != 0
 
 
 def alert_use_notification_p(alert):
     return alert[6] == 'y'
 
 
-def run_alerts():
+def run_alerts(date):
     """Run the alerts.
 
     First decide which alerts to run according to the current local
     time, and runs them."""
-    
-    t = localtime()
-    if t[2] == 1: # first of the month
-        process_alert_queries('month')
-        
-    t = strftime("%A")
-    if t == 'Monday': # first day of the week
-        process_alert_queries('week')
+
+    if date.day == 1:
+        process_alert_queries('month', date)
+
+    if date.isoweekday() == 1: # first day of the week
+        process_alert_queries('week', date)
         
-    process_alert_queries('day')
+    process_alert_queries('day', date)
 
-def process_alert_queries_for_user(uid):
+def process_alert_queries_for_user(uid, date):
     """Process the alerts for the given user id.
 
     All alerts are with reference date set as the current local time."""
     
     alert_queries = get_alert_queries_for_user(uid)
     print alert_queries
 
     for aq in alert_queries:
         frequency = aq[2]
-        q = run_query(aq, frequency)
+        q = run_query(aq, frequency, date)
         alerts = get_alerts(q, frequency)
         process_alerts(alerts)
 
     
 if __name__ == '__main__':
     process_alert_queries_for_user(2571422) # erik
     process_alert_queries_for_user(109) # tibor
     # process_alert_queries_for_user(11040) # jean-yves
diff --git a/modules/webalert/lib/htmlparser.py b/modules/webalert/lib/htmlparser.py
index 75162f3a5..03440a5d2 100644
--- a/modules/webalert/lib/htmlparser.py
+++ b/modules/webalert/lib/htmlparser.py
@@ -1,126 +1,122 @@
 ## $Id$
 ## HTML parser for records.
 
 ## This file is part of the CERN Document Server Software (CDSware).
 ## Copyright (C) 2002, 2003, 2004, 2005 CERN.
 ##
 ## The CDSware is free software; you can redistribute it and/or
 ## modify it under the terms of the GNU General Public License as
 ## published by the Free Software Foundation; either version 2 of the
 ## License, or (at your option) any later version.
 ##
 ## The CDSware is distributed in the hope that it will be useful, but
 ## WITHOUT ANY WARRANTY; without even the implied warranty of
 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 ## General Public License for more details.  
 ##
 ## You should have received a copy of the GNU General Public License
 ## along with CDSware; if not, write to the Free Software Foundation, Inc.,
 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 
 """HTML parser for records."""
 
 ## rest of the Python code goes below
 
 __version__ = "$Id$"
 
 from HTMLParser import HTMLParser
 from string import split
 
 from cdsware.config import *
 from cdsware.search_engine import print_record
 from cdsware import textwrap
 
 WRAPWIDTH = 72
 
 def wrap(text):
-    global WRAPWIDTH
-    
     lines = textwrap.wrap(text, WRAPWIDTH)
     r = ''
     for l in lines:
         r += l + '\n'
     return r
 
 def wrap_records(text):
-    global WRAPWIDTH
-    
     lines = split(text, '\n')
     result = ''
     for l in lines:
         newlines = textwrap.wrap(l, WRAPWIDTH)
         for ll in newlines:
             result += ll + '\n'
     return result
 
 class RecordHTMLParser(HTMLParser):
     """A parser for the HTML returned by cdsware.search_engine.print_record.
 
     The parser provides methods to transform the HTML returned by
     cdsware.search_engine.print_record into plain text, with some
     minor formatting.
     """
     
     def __init__(self):
         HTMLParser.__init__(self)
         self.result = ''
         
     def handle_starttag(self, tag, attrs):
         if tag == 'strong':
             # self.result += '*'
             pass
         elif tag == 'a':
             self.printURL = 0
             self.unclosedBracket = 0
             for f in attrs:
                 if f[1] == 'note':
                     self.result += 'Fulltext : <'
                     self.unclosedBracket = 1
                 if f[1] == 'moreinfo':
                     self.result += 'Detailed record : '
                     self.printURL = 1
                 if (self.printURL == 1) and (f[0] == 'href'):
                     self.result += '<' + f[1] + '>'
                 
         elif tag == 'br':
             self.result += '\n'
         
     def handle_endtag(self, tag):
         if tag == 'strong':
             # self.result += '\n'
             pass
         elif tag == 'a':
             if self.unclosedBracket == 1:
                 self.result += '>'
                 self.unclosedBracket = 0
 
     def handle_data(self, data):
         if data == 'Detailed record':
             pass
         else:
             self.result += data
 
     def handle_comment(self, data):
         pass
     
 
 def get_as_text(record_id):
     """Return the plain text from RecordHTMLParser of the record."""
 
     rec = print_record(record_id)
     htparser = RecordHTMLParser()
     try:
         htparser.feed(rec)
         return htparser.result
     except:
         #htparser.close()
         return wrap(htparser.result + 'Detailed record: <http://cdsweb.cern.ch/search.py?recid=%s>.' % record_id)
 
 
 if __name__ == "__main__":
     rec = print_record(619028)
     print rec
     
     print "***"
     
     print get_as_text(619028)
diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py
index cf1a94a55..a53aa1898 100644
--- a/modules/webalert/lib/webalert_templates.py
+++ b/modules/webalert/lib/webalert_templates.py
@@ -1,465 +1,553 @@
 ## $Id$
 
 ## This file is part of the CERN Document Server Software (CDSware).
 ## Copyright (C) 2002, 2003, 2004, 2005 CERN.
 ##
 ## The CDSware is free software; you can redistribute it and/or
 ## modify it under the terms of the GNU General Public License as
 ## published by the Free Software Foundation; either version 2 of the
 ## License, or (at your option) any later version.
 ##
 ## The CDSware is distributed in the hope that it will be useful, but
 ## WITHOUT ANY WARRANTY; without even the implied warranty of
 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 ## General Public License for more details.  
 ##
 ## You should have received a copy of the GNU General Public License
 ## along with CDSware; if not, write to the Free Software Foundation, Inc.,
 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 
 import urllib
 import time
 import cgi
 import gettext
 import string
 import locale
 import re
 import operator
 
 from cdsware.config import *
 from cdsware.messages import gettext_set_language
+from cdsware.htmlparser import get_as_text, wrap
 
 class Template:
     def tmpl_errorMsg(self, ln, error_msg, rest = ""):
         """
         Adds an error message to the output
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'error_msg' *string* - The error message
 
           - 'rest' *string* - The rest of the page
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         out = """<div class="quicknote">%(error)s</div><br>%(rest)s""" % {
                 'error' : error_msg,
                 'rest' : rest
               }
         return out
 
     def tmpl_textual_query_info_from_urlargs(self, ln, args):
         """
         Displays a human inteligible textual representation of a query
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'args' *array* - The URL arguments array (parsed)
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         out = ""
         if args.has_key('p'):
             out += "<strong>" + _("Pattern") + ":</strong> " + string.join(args['p'], "; ") + "<br>"
         if args.has_key('f'):
             out += "<strong>" + _("Field") + ":</strong> " + string.join(args['f'], "; ") + "<br>"
         if args.has_key('p1'):
             out += "<strong>" + _("Pattern 1") + ":</strong> " + string.join(args['p1'], "; ") + "<br>"
         if args.has_key('f1'):
             out += "<strong>" + _("Field 1") + ":</strong> " + string.join(args['f1'], "; ") + "<br>"
         if args.has_key('p2'):
             out += "<strong>" + _("Pattern 2") + ":</strong> " + string.join(args['p2'], "; ") + "<br>"
         if args.has_key('f2'):
             out += "<strong>" + _("Field 2") + ":</strong> " + string.join(args['f2'], "; ") + "<br>"
         if args.has_key('p3'):
             out += "<strong>" + _("Pattern 3") + ":</strong> " + string.join(args['p3'], "; ") + "<br>"
         if args.has_key('f3'):
             out += "<strong>" + _("Field 3") + ":</strong> " + string.join(args['f3'], "; ") + "<br>"
         if args.has_key('c'):
             out += "<strong>" + _("Collections") + ":</strong> " + string.join(args['c'], "; ") + "<br>"
         elif args.has_key('cc'):
             out += "<strong>" + _("Collection") + ":</strong> " + string.join(args['cc'], "; ") + "<br>"
         return out
 
     def tmpl_account_list_alerts(self, ln, alerts):
         """
         Displays all the alerts in the main "Your account" page
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'alerts' *array* - The existing alerts IDs ('id' + 'name' pairs)
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         out = """<FORM name="displayalert" action="../youralerts.py/list" method="post">
                  %(you_own)s
                 <SELECT name="id_alert">
                   <OPTION value="0">- %(alert_name)s -</OPTION>""" % {
                  'you_own' : _("You own following alerts:"),
                  'alert_name' : _("alert name"),
                }
         for alert in alerts :
                   out += """<OPTION value="%(id)s">%(name)s</OPTION>""" % alert
         out += """</SELECT>
                 &nbsp;<CODE class="blocknote">
                 <INPUT class="formbutton" type="submit" name="action" value="%(show)s"></CODE>
                 </FORM>""" % {
                   'show' : _("SHOW"),
                 }
         return out
 
     def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notification, baskets, old_id_basket, id_basket, id_query):
         """
         Displays an alert adding form.
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'query' *string* - The HTML code of the textual representation of the query (as returned ultimately by tmpl_textual_query_info_from_urlargs...)
 
           - 'alert_name' *string* - The alert name
 
           - 'action' *string* - The action to complete ('update' or 'add')
 
           - 'frequency' *string* - The frequency of alert running ('day', 'week', 'month')
 
           - 'notification' *string* - If notification should be sent by email ('y', 'n')
 
           - 'baskets' *array* - The existing baskets ('id' + 'name' pairs)
 
           - 'old_id_basket' *string* - The id of the previous basket of this alert
 
           - 'id_basket' *string* - The id of the basket of this alert
 
           - 'id_query' *string* - The id of the query associated to this alert
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         out = ""
         out += """<TABLE border="0" cellspacing="0" cellpadding="2" width="650">
                     <TR><TD colspan="3">%(notify_cond)s </TD></TR>
                     <TR>
                       <TD>&nbsp;&nbsp;</TD>
                       <TD align="left" valign="top" width="10"><B>%(query_text)s:</B></TD>
                       <TD align="left" valign="top" width="500">%(query)s</TD></TR>
                   </TABLE>""" % {
                  'notify_cond' : _("This alert will notify you each time/only if a new item satisfy the following query"),
                  'query_text' : _("QUERY"),
                  'query' : query,
                }
 
         out += """<FORM name="setalert" action="../youralerts.py/%(action)s" method="get">
         <TABLE style="background-color:F1F1F1; border:thin groove grey" cellspacing="0" cellpadding="0"><TR><TD>
                     <TABLE border="0" cellpadding="0" cellspacing ="10">
                       <TR>
                         <TD align="right" valign="top"><B>%(alert_name)s</B></TD>
                         <TD><INPUT type="text" name="name" size="20" maxlength="50" value="%(alert)s"></TD>
                       </TR>
                       <TR><TD align="right"><B>%(freq)s</B></TD>
                           <TD><SELECT name="freq">
                                 <OPTION value="month" %(freq_month)s>%(monthly)s</OPTION>
                              <OPTION value="week" %(freq_week)s>%(weekly)s</OPTION>
                              <OPTION value="day" %(freq_day)s>%(daily)s</OPTION></SELECT>
                          </TD>
                       </TR>
                       <TR>
                         <TD align="right"><B>%(send_email)s</B></TD>
                         <TD><SELECT name="notif">
                             <OPTION value="y" %(notif_yes)s>%(yes)s</OPTION>
                             <OPTION value="n"%(notif_no)s>%(no)s</OPTION></SELECT>
                             <SMALL class="quicknote"> (%(specify)s)</SMALL>&nbsp;
                         </TD>
                       </TR>
                       <TR>
                         <TD align="right" valign="top"><B>%(store_basket)s</B></TD>
                         <TD><SELECT name="idb"><OPTION value="0">- %(no_basket)s -</OPTION>
                """ % {
                  'action': action,
                  'alert_name' : _("Alert identification name:"),
                  'alert' : alert_name,
                  'freq' : _("Search-checking frequency:"),
                  'freq_month' : (frequency == 'month' and "selected" or ""),
                  'freq_week' : (frequency == 'week' and "selected" or ""),
                  'freq_day' : (frequency == 'day' and "selected" or ""),
                  'monthly' : _("monthly"),
                  'weekly' : _("weekly"),
                  'daily' : _("daily"),
                  'send_email' : _("Send notification e-mail?"),
                  'notif_yes' : (notification == 'y' and "selected" or ""),
                  'notif_no' : (notification == 'n' and "selected" or ""),
                  'yes' : _("yes"),
                  'no' : _("no"),
                  'specify' : _("if <B>no</B> you must specify a basket"),
                  'store_basket' : _("Store results in basket?"),
                  'no_basket' : _("no basket"),
                }
         for basket in baskets:
             out += """<OPTION value="%(id)s" %(selected)s>%(name)s</OPTION>""" % {
                      'id' : basket['id'],
                      'name' : basket['name'],
                      'selected' : (basket['id'] == id_basket and "selected" or "")
                    }
 
         out += """   </SELECT><BR><SMALL>%(insert_basket)s</SMALL><BR>
                      <INPUT type="text" name="bname" size="20" maxlength="50">
                     </TD>
                    </TR>
                    <TR>
                     <TD colspan="2" align="center"><BR>
                       <INPUT type="hidden" name="idq" value="%(idq)s">
                       <CODE class="blocknote"><INPUT class="formbutton" type="submit" name="action" value="&nbsp;%(set_alert)s&nbsp;"></CODE>&nbsp;
                       <CODE class="blocknote"><INPUT class="formbutton" type="reset" value="%(clear_data)s"></CODE>
                      </TD>
                     </TR>
                    </TABLE>
                   </TD>
                  </TR>
                 </TABLE>
                """ % {
                      'insert_basket' : _("or insert a new basket name"),
                      'idq' : id_query,
                      'set_alert' : _("SET ALERT"),
                      'clear_data' : _("CLEAR DATA"),
                    }
         if action == "update":
             out += """<INPUT type="hidden" name="old_idb" value="%s">""" % old_id_basket
         out += "</FORM>"
 
         return out
 
     def tmpl_list_alerts(self, ln, weburl, alerts, guest, guesttxt):
         """
         Displays the list of alerts
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'weburl' *string* - The url of cdsware
 
           - 'alerts' *array* - The existing alerts:
 
               - 'queryid' *string* - The id of the associated query
 
               - 'queryargs' *string* - The query string
 
               - 'textargs' *string* - The textual description of the query string
 
               - 'userid' *string* - The user id
 
               - 'basketid' *string* - The basket id
 
               - 'basketname' *string* - The basket name
 
               - 'alertname' *string* - The alert name
 
               - 'frequency' *string* - The frequency of alert running ('day', 'week', 'month')
 
               - 'notification' *string* - If notification should be sent by email ('y', 'n')
 
               - 'created' *string* - The date of alert creation
 
               - 'lastrun' *string* - The last running date
 
           - 'guest' *bool* - If the user is a guest user
 
           - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user)
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         out = """<P>%(set_new_alert)s</P>""" % {
                 'set_new_alert' : _("Set a new alert from %(your_searches)s, the %(popular_searches)s or the input form.") % {
                                     'your_searches' : """<A href="display">%s</A>""" % _("your searches"),
                                     'popular_searches' : """<A href="display?p='y'">%s</A>""" % _("most popular searches"),
                                   }
               }
 
         if len(alerts):
               out += """<TABLE border="1" cellspacing="0" cellpadding="3" width="100%%">
                           <TR class="pageboxlefttop" align="center">
                             <TD><B>%(no)s</B></TD>
                             <TD><B>%(name)s</B></TD>
                             <TD><B>%(search_freq)s</B></TD>
                             <TD><B>%(notification)s</B></TD>
                             <TD><B>%(result_basket)s</B></TD>
                             <TD><B>%(date_run)s</B></TD>
                             <TD><B>%(date_created)s</B></TD>
                             <TD><B>%(query)s</B></TD>
                             <TD><B>%(action)s</B></TD></TR>""" % {
                        'no' : _("No"),
                        'name' : _("Name"),
                        'search_freq' : _("Search checking frequency"),
                        'notification' : _("Notification by e-mail"),
                        'result_basket' : _("Result in basket"),
                        'date_run' : _("Date last run"),
                        'date_created' : _("Creation date"),
                        'query' : _("Query"),
                        'action' : _("Action"),
                      }
               i = 0
               for alert in alerts:
                   i += 1
                   if alert['frequency'] == "day":
                       frequency = _("daily"),
                   else:
                       if alert['frequency'] == "week":
                           frequency = _("weekly")
                       else:
                           frequency = _("monthly")
 
                   if alert['notification'] == "y":
                       notification = _("yes")
                   else:
                       notification = _("no")
 
                   out += """<TR>
                               <TD><I>#%(index)d</I></TD>
                              <TD><B><NOBR>%(alertname)s<NOBR></B></TD>
                              <TD>%(frequency)s</TD>
                              <TD align="center">%(notification)s</TD>
                              <TD><NOBR>%(basketname)s<NOBR></TD>
                              <TD><NOBR>%(lastrun)s<NOBR></TD>
                              <TD><NOBR>%(created)s<NOBR></TD>
                              <TD>%(textargs)s</TD>
                              <TD><A href="./remove?name=%(alertname)s&idu=%(userid)d&idq=%(queryid)d&idb=%(basketid)d">%(remove)s</A><BR>
                                  <A href="./modify?idq=%(queryid)d&name=%(alertname)s&freq=%(freq)s&notif=%(notif)s&idb=%(basketid)d&old_idb=%(basketid)d">%(modify)s</A><BR>
                                  <A href="%(weburl)s/search.py?%(queryargs)s">%(search)s</A>
                              </TD>
                             </TR>""" % {
                     'index' : i,
                     'alertname' : alert['alertname'],
                     'frequency' : frequency,
                     'notification' : notification,
                     'basketname' : alert['basketname'],
                     'lastrun' : alert['lastrun'],
                     'created' : alert['created'],
                     'textargs' : alert['textargs'],
                     'userid' : alert['userid'],
                     'queryid' : alert['queryid'],
                     'basketid' : alert['basketid'],
                     'freq' : alert['frequency'],
                     'notif' : alert['notification'],
                     'remove' : _("Remove"),
                     'modify' : _("Modify"),
                     'weburl' : weburl,
                     'search' : _("Execute search"),
                     'queryargs' : alert['queryargs']
                   }
 
               out += '</TABLE>'
               
         out += """<P>%(defined)s</P>""" % {
                  'defined' : _("You have defined <B>%(number)s</B> alerts.") % { 'number' : len(alerts)}
                }
 
         if guest:
             out += guesttxt
 
         return out
 
     def tmpl_display_alerts(self, ln, weburl, permanent, nb_queries_total, nb_queries_distinct, queries, guest, guesttxt):
         """
         Displays the list of alerts
 
         Parameters:
 
           - 'ln' *string* - The language to display the interface in
 
           - 'weburl' *string* - The url of cdsware
 
           - 'permanent' *string* - If displaying most popular searches ('y') or only personal searches ('n')
 
           - 'nb_queries_total' *string* - The number of personal queries in the last period
 
           - 'nb_queries_distinct' *string* - The number of distinct queries in the last period
 
           - 'queries' *array* - The existing queries:
 
               - 'id' *string* - The id of the associated query
 
               - 'args' *string* - The query string
 
               - 'textargs' *string* - The textual description of the query string
 
               - 'lastrun' *string* - The last running date (only for personal queries)
 
           - 'guest' *bool* - If the user is a guest user
 
           - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user)
         """
 
         # load the right message language
         _ = gettext_set_language(ln)
 
         if len(queries) == 0:
             return _("You have not executed any search yet. %(click_here)s for search.") % {
                      'click_here' : """<a href="%(weburl)s/search.py">%(click)s</a>""" % {
                                       'weburl' : weburl,
                                       'click' : _("Click here"),
                                     }
                    }
 
         out = ''
         
         # display message: number of items in the list
         if permanent=="n":
             out += """<P>""" + _("You have performed <B>%(number)d</B> searches (<strong>%(different)d</strong> different questions) during the last 30 days or so.""") % {
                      'number' : nb_queries_total,
                      'different' : nb_queries_distinct
                    } + """</P>"""
         else:
             # permanent="y"
             out += """<P>Here are listed the <B>%s</B> most popular searches.</P>""" % len(query_result)
 
         # display the list of searches
         out += """<TABLE border="1" cellspacing="0" cellpadding="3" width="100%%">
                     <TR class="pageboxlefttop"><TD><B>%(no)s</B></TD><TD><B>%(question)s</B></TD>
                     <TD><B>%(action)s</B></TD>""" % {
                       'no' : _("#"),
                       'question' : _("Question"),
                       'action' : _("Action")
                     }
         if permanent=="n":
             out += """<TD><B>%s</B></TD>""" % _("Last Run")
         out += """</TR>\n"""
         i = 0
         for query in queries :
             i += 1
             # id, pattern, base, search url and search set alert, date
             out += """<TR>
                         <TD><I>#%(index)d</I></TD>
                         <TD>%(textargs)s</TD>
                         <TD><A href="%(weburl)s/search.py?%(args)s">%(execute_query)s</A><BR><A href="./input?idq=%(id)d">%(set_alert)s</A></TD>""" % {
                      'index' : i,
                      'textargs' : query['textargs'],
                      'weburl' : weburl,
                      'args' : query['args'],
                      'id' : query['id'],
                      'execute_query' : _("Execute search"),
                      'set_alert' : _("Set new alert")
                    }
             if permanent=="n":
                 out += """<TD>%(lastrun)s</TD>""" % query
             out += """</TR>\n"""
         out += """</TABLE><BR>\n"""
         if guest :
             out += guesttxt
 
         return out
+
+    def tmpl_alert_email_headers(self, name, headers):
+        
+        headers['Subject'] = 'Alert %s run on %s' % (
+            name, time.strftime("%Y-%m-%d"))
+        
+        headers['From'] = 'CDS Alert Engine <%s>' % alertengineemail
+
+    
+    def tmpl_alert_email_body(self, name, url, records, pattern,
+                              catalogues, frequency):
+
+        MAXIDS = 5
+
+
+        l = len(catalogues)
+        if l == 0:
+            collections = ''
+        elif l == 1:
+            collections = "collection: %s\n" % catalogues[0]
+        else:
+            collections = "collections: %s\n" % wrap(', '.join(catalogues))
+
+        if pattern:
+            pattern = 'pattern: %s\n' % pattern
+
+        frequency = {'day': 'daily',
+                     'month': 'monthly',
+                     'year': 'yearly'}[frequency]
+
+        l = len(records)
+        if l == 1:
+            total = '1 record'
+        else:
+            total = '%d records' % l
+
+        
+        body = """\
+Hello,
+
+Below are the results of the email alert that you set up with the CERN
+Document Server. This is an automatic message, please don't reply to
+its address.  For any question, use <%(supportemail)s> instead.
+
+alert name: %(name)s
+%(pattern)s%(collections)sfrequency: %(frequency)s
+run time: %(runtime)s
+found: %(total)s
+url: <%(url)s>
+
+
+""" % {'supportemail': supportemail,
+       'name': name,
+       'pattern': pattern,
+       'collections': collections,
+       'frequency': frequency,
+       'runtime': time.strftime("%a %Y-%m-%d %H:%M:%S"),
+       'total': total,
+       'url': url}
+        
+        
+        for index, recid in enumerate(records[:MAXIDS]):
+            body += self.tmpl_alert_email_record(index, recid)
+
+        if len(records) > MAXIDS:
+            body += '''
+
+Only the first %s records were displayed.  Please consult the search
+URL given at the top of this email to see all the results.
+''' % MAXIDS
+
+
+        body += '''
+
+-- 
+CERN Document Server Alert Service <%s>
+Unsubscribe?  See <%s>
+Need human intervention?  Contact <%s>
+''' % (weburl, weburl + '/youralerts.py/list', supportemail)
+        
+        return body
+
+
+    def tmpl_alert_email_record(self, index, recid):
+        """ Format a single record."""
+
+        return wrap('\n\n%s) %s' % (index+1, get_as_text(recid))) + '\n'