pimdb_bb.py 23.8 KB
Newer Older
1
##
2
## Created : Sat Apr 07 18:52:19 IST 2012
3
##
4
## Copyright (C) 2012, 2013 by Sriram Karra <karra.etc@gmail.com>
5
##
6 7 8 9 10 11 12 13 14 15 16 17 18
## This file is part of ASynK
##
## ASynK is free software: you can redistribute it and/or modify it under
## the terms of the GNU Affero General Public License as published by the
## Free Software Foundation, version 3 of the License
##
## ASynK 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 Affero General Public
## License for more details.
##
## You should have a copy of the license in the doc/ directory of ASynK.  If
## not, see <http://www.gnu.org/licenses/>.
19 20
##

21
import codecs, datetime, logging, os, re, shutil, string, time
22 23 24
from   pimdb        import PIMDB
from   folder       import Folder
from   folder_bb    import BBContactsFolder
25
from   contact_bb   import BBContact, BBDBParseError
26
import utils
Sriram Karra's avatar
Sriram Karra committed
27

28 29 30
class BBDBFileFormatError(Exception):
    pass

31 32 33
class ASynKBBDBUnicodeError(Exception):
    pass

Sriram Karra's avatar
Sriram Karra committed
34 35 36 37 38 39 40 41 42 43 44 45 46
## Note: Each BBDB File is a message store and there are one or more folders
## in it.
class MessageStore:
    """Represents a physical BBDB file, made up of one or more folders,
    each containing contacts."""

    def __init__ (self, db, name):
        self.atts = {}
        self.set_db(db)
        self.set_name(name)
        self.set_folders({})

        self.populate_folders()
47 48

    ##
Sriram Karra's avatar
Sriram Karra committed
49
    ## Some get and set routines
50 51
    ##

Sriram Karra's avatar
Sriram Karra committed
52 53
    def _get_att (self, key):
        return self.atts[key]
54

Sriram Karra's avatar
Sriram Karra committed
55 56 57
    def _set_att (self, key, val):
        self.atts[key] = val
        return val
58

Sriram Karra's avatar
Sriram Karra committed
59 60
    def get_db (self):
        return self._get_att('db')
61

Sriram Karra's avatar
Sriram Karra committed
62 63
    def set_db (self, db):
        return self._set_att('db', db)
64

Sriram Karra's avatar
Sriram Karra committed
65 66
    def get_config (self):
        return self.get_db().get_config()
67

Sriram Karra's avatar
Sriram Karra committed
68 69
    def get_name (self):
        return self._get_att('name')
70

Sriram Karra's avatar
Sriram Karra committed
71 72
    def set_name (self, name):
        return self._set_att('name', name)
73

Sriram Karra's avatar
Sriram Karra committed
74 75
    def get_store_id (self):
        return self.get_name()
76

Sriram Karra's avatar
Sriram Karra committed
77 78
    def get_folders (self):
        return self.folders
79

Sriram Karra's avatar
Sriram Karra committed
80 81 82 83 84
    def get_folder (self, name):
        if name in self.folders:
            return self.folders[name]
        else:
            return None
85

Sriram Karra's avatar
Sriram Karra committed
86 87
    def add_folder (self, f):
        self.folders.update({f.get_name() : f})
88

89 90 91 92 93 94
    def remove_folder (self, fold):
        """Remove a folder from the folder list by name."""

        del self.folders[fold.get_name()]
        self.save_file()

Sriram Karra's avatar
Sriram Karra committed
95 96
    def set_folders (self, fs):
        """fs has to be a dictionary"""
97

Sriram Karra's avatar
Sriram Karra committed
98
        self.folders = fs
Sriram Karra's avatar
Sriram Karra committed
99

100
    ##
Sriram Karra's avatar
Sriram Karra committed
101
    ## The Real Action
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    ##

    def get_con_re (self):
        return self._get_att('con_re')

    def set_con_re (self, reg):
        return self._set_att('con_re', reg)

    def get_str_re (self):
        return self._get_att('str_re')

    def set_str_re (self, reg):
        return self._set_att('str_re', reg)

    def get_adr_re (self):
        return self._get_att('adr_re')

    def set_adr_re (self, reg):
        return self._set_att('adr_re', reg)

    def get_ph_re (self):
        return self._get_att('ph_re')

    def set_ph_re (self, reg):
        return self._set_att('ph_re', reg)

    def get_note_re (self):
        return self._get_att('note_re')

    def set_note_re (self, reg):
        return self._set_att('note_re', reg)

    def get_notes_re (self):
        return self._get_att('notes_re')

    def set_notes_re (self, reg):
        return self._set_att('notes_re', reg)

Sriram Karra's avatar
Sriram Karra committed
140 141 142 143 144 145
    def get_sync_tag_re (self):
        return self._get_att('sync_tag_re')

    def set_sync_tag_re (self, reg):
        return self._set_att('sync_tag_re', reg)

Sriram Karra's avatar
Sriram Karra committed
146 147 148 149
    @classmethod
    def get_def_folder_name (self):
        return 'default'

150 151 152 153 154
    def _set_regexes (self, ver=None):
        if not ver:
            ver     = self.get_file_format()
        regexes = self.get_db().get_regexes(ver)
        
155
        ## Now save some of the regexes for later use...
156 157 158 159 160 161
        self.set_con_re(regexes['con_re'])
        self.set_str_re(regexes['str_re'])
        self.set_adr_re(regexes['adr_re'])
        self.set_ph_re(regexes['ph_re'])
        self.set_note_re(regexes['note_re'])
        self.set_notes_re(regexes['notes_re'])
Sriram Karra's avatar
Sriram Karra committed
162 163 164 165 166 167 168 169

        # Compute and store away a regular expression to match sync tags in
        # the notes section
        c = self.get_config()
        p = c.get_label_prefix()
        s = c.get_label_separator()
        r = '%s%s\w+%s' % (p, s, s)
        self.set_sync_tag_re(r)
Sriram Karra's avatar
Sriram Karra committed
170

171 172 173 174 175 176
    def set_encoding (self, ver):
        return self._set_att('encoding', ver)

    def get_encoding (self):
        return self._get_att('encoding')

Sriram Karra's avatar
Sriram Karra committed
177 178 179 180 181 182
    def set_file_format (self, ver):
        return self._set_att('file_format', ver)

    def get_file_format (self):
        return self._get_att('file_format')

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
    def get_preamble (self):
        return self._get_att('preamble')

    def set_preamble (self, pre):
        return self._set_att('preamble', pre)

    def append_preamble (self, lines):
        try:
            pre = self.get_preamble()
        except KeyError, e:
            pre = ''

        pre += lines
        return self.set_preamble(pre)

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    def _parse_preamble (self, fn, bbf):
        ff = bbf.readline()
        if re.search('coding:', ff):
            # Ignore first line if such: ;; -*-coding: utf-8-emacs;-*-
            self.append_preamble(ff)
            ff = bbf.readline()

        # Processing: ;;; file-format: 8
        res = re.search(';;; file-(format|version):\s*(\d+)', ff)
        if not res:
            bbf.close()
            raise BBDBFileFormatError('Unrecognizable format line: %s' % ff)

        self.append_preamble(ff)
        ver = res.group(2)
        self.set_file_format(ver)

        supported = self.get_db().supported_file_formats()
        if not ver in supported:
            bbf.close()
            raise BBDBFileFormatError(('Cannot process file "%s" '
                                       '(version %s). Supported versions '
                                       'are: %s' % (fn, ver, supported)))

        return ver

224 225 226 227
    def parse_with_encoding (self, def_f, fn, encoding):
        """Folder object to which the parsed contacts will be added. fn is the
        name of the BBDB file/message store. encoding is a string representing
        a text encoding such as utf-8, latin-1, etc."""
228

229
        with codecs.open(fn, encoding=encoding) as bbf:
230
            ver = self._parse_preamble(fn, bbf)
231

232
            ## Now fetch and set up the parsing routines specific to the file
233 234
            ## format 
            self._set_regexes(ver)
Sriram Karra's avatar
Sriram Karra committed
235 236 237

            cnt = 0
            while True:
238 239 240 241
                try:
                    ff = bbf.readline()
                except UnicodeDecodeError, e:
                    ## We got the encoding wrong. We will have to drop
242 243
                    ## everything we have done, and start all over again.  At
                    ## a later stage, we could optimize by skipping over
244 245 246 247 248
                    ## whatever we have read so far, but then we will need to
                    ## evalute if the parsed strings will be in the same
                    ## encoding or not. Tricky and shady business, this.
                    raise ASynKBBDBUnicodeError('')

Sriram Karra's avatar
Sriram Karra committed
249 250 251 252
                if ff == '':
                    break

                if re.search('^;', ff):
253
                    self.append_preamble(ff)
Sriram Karra's avatar
Sriram Karra committed
254 255
                    continue

256 257 258
                try:
                    c  = BBContact(def_f, rec=ff.rstrip())
                except BBDBParseError, e:
259
                    logging.error('Could not parse BBDB record: %s', ff)
260

261
                    raise BBDBFileFormatError(('Cannot proceed with '
262
                                              'processing file "%s" ') % fn)
263 264

                fon = c.get_bbdb_folder()
Sriram Karra's avatar
Sriram Karra committed
265

266 267
                if fon:
                    f = self.get_folder(fon)
Sriram Karra's avatar
Sriram Karra committed
268
                    if not f:
269
                        f = BBContactsFolder(self.get_db(), fon, self)
Sriram Karra's avatar
Sriram Karra committed
270 271 272 273 274 275 276
                        self.add_folder(f)
                    f.add_contact(c)
                else:
                    def_f.add_contact(c)

                cnt += 1

277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
            return bbf, cnt

    def populate_folders (self, fn=None):
        """Parse a BBDB file contents, and create folders of contacts."""

        ## BBDB itself is not structured as logical folders. The concept of a
        ## BBDB folder is overlayed by ASynK. Any contact with a notes field
        ## with key called 'folder' (or as configured in config.json), is
        ## assigned to a folder of that name. If an object does not have a
        ## folder note, it is assgined to the default folder.

        ## This routine parses the BBDB file by reading one line at at time
        ## from top to bottom. Due to a limitation in how the Contact() and
        ## Folder() classes interact, we have to pass a valid Folder object to
        ## the Contact() constructor. So the way we do this is we start by
        ## assuming the contact is in the default folder. After successful
        ## parsing, if the folder name is available in the contact, we will
        ## move it from the dfault folder to that particular folder.

        if not fn:
            fn = self.get_name()

        fn = utils.abs_pathname(self.get_config(), fn)
        logging.info('Parsing BBDB file %s...', fn)

        def_fn = self.get_def_folder_name()
        def_f = BBContactsFolder(self.get_db(), def_fn, self)
        self.add_folder(def_f)
305
        failed = True
306

307
        for encoding in self.get_db().get_text_encodings():
308 309 310 311 312 313 314 315
            self.set_encoding(encoding)
            try:
                logging.info('Parsing BBDB Store with encoding %s...',
                             encoding)
                bbf, cnt = self.parse_with_encoding(def_f, fn,
                                                    encoding=encoding)
                logging.info('Parsing BBDB Store with encoding %s...Success',
                             encoding)
316
                failed = False
317 318 319
                break
            except ASynKBBDBUnicodeError, e:
                ## Undo all state, and start afresh, pretty much.
320
                failed = True
321 322 323 324 325 326 327 328
                self.set_file_format(0)
                self.set_preamble('')
                self.set_folders({})
                def_f = BBContactsFolder(self.get_db(), def_fn, self)
                self.add_folder(def_f)
                logging.info('Parsing BBDB Store with encoding %s...Failed',
                             encoding)

329 330 331 332 333
        if failed:
            ## Oops, we failed to parse the file fully even once...
            raise BBDBFileFormatError('Cannot process file "%s": unable to '
                                      'ascerain text encoding.' % fn)

Sriram Karra's avatar
Sriram Karra committed
334 335 336 337 338
        logging.info('Successfully parsed %d entries.', cnt)
        bbf.close()

    def save_file (self, fn=None):
        if not fn:
339
            fn = self.get_name()
Sriram Karra's avatar
Sriram Karra committed
340

341
        fn = utils.abs_pathname(self.get_config(), fn)
342
        logging.info('Saving BBDB File %s...', fn)
Sriram Karra's avatar
Sriram Karra committed
343

344
        with codecs.open(fn, 'w', encoding=self.get_encoding()) as bbf:
345
            bbf.write(self.get_preamble())
Sriram Karra's avatar
Sriram Karra committed
346

347 348
            for name, f in self.get_folders().iteritems():
                f.write_to_file(bbf)
Sriram Karra's avatar
Sriram Karra committed
349 350 351 352 353 354 355 356 357 358

        bbf.close()

class BBPIMDB(PIMDB):
    """Wrapper class over the BBDB, by implementing the PIMDB abstract
    class."""

    def __init__ (self, config, def_fn):
        PIMDB.__init__(self, config)

359 360 361 362
        ## Setup some BBDB specific config parameters
        enc = self.get_db_config()['text_encodings']
        self.set_text_encodings(enc)

363 364 365
        ## For now the only version we support is file format 7. But in the
        ## near future ...
        self.set_regexes({})
366
        self._set_regexes_ver6()
367
        self._set_regexes_ver7()
Sriram Karra's avatar
Sriram Karra committed
368

369
        self.set_msgstores({})
Sriram Karra's avatar
Sriram Karra committed
370 371 372 373 374 375 376 377
        def_ms = self.add_msgstore(def_fn)
        self.set_def_msgstore(def_ms)
        self.set_folders()

    ##
    ## First implementation of the abstract methods of PIMDB.
    ##

378 379 380
    def supported_file_formats (self):
        return self.get_regexes().keys()

Sriram Karra's avatar
Sriram Karra committed
381 382 383 384 385
    def get_dbid (self):
        """See the documentation in class PIMDB"""

        return 'bb'

386 387 388 389 390 391 392
    def set_text_encodings (self, name):
        self.text_encodings = name
        return name

    def get_text_encodings (self):
        return self.text_encodings

393 394 395
    def get_msgstore (self, name):
        return self.msgstores[name]

Sriram Karra's avatar
Sriram Karra committed
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    def get_msgstores (self):
        return self.msgstores

    def set_msgstores (self, ms):
        self.msgstores = ms
        return ms

    def get_def_msgstore (self):
        return self.def_msgstore

    def set_def_msgstore (self, ms):
        self.def_msgstore = ms
        return ms

    def add_msgstore (self, ms):
        """Add another messagestore to the PIMDB. ms can be either a string,
        or an object of type MessageStore. If it is a string, then the string
        is interpreted as the fully expanded name of a BBDB file, and it is
        parsed accordingly. If it is an object already, then it is simply
        appended to the existing list of stores."""

        if isinstance(ms, MessageStore):
            self.msgstores.update({ms.get_name(): ms})
        elif isinstance(ms, basestring):
            ms = MessageStore(self, ms)
            self.msgstores.update({ms.get_name() : ms})
        else:
            logging.error('Unknown type (%s) in argument to add_msgstore %s',
                          type(ms), ms)
            return None

        return ms

429 430 431 432 433
    def get_regexes (self, ver=None):
        if ver:
            return self.regexes[ver]
        else:
            return self.regexes
434 435 436 437 438 439 440 441

    def set_regexes (self, rg):
        self.regexes = rg
        return rg

    def add_regexes (self, ver, value):
        self.regexes.update({ver : value})

442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
    def new_folder (self, fname, ftype=None, storeid=None):
        logging.debug('bb:new_folder(): fname: %s; ftype: %s', fname, ftype)
        if not ftype:
            ftype = Folder.CONTACT_t

        if ftype != Folder.CONTACT_t:
            logging.erorr('Only Contact Groups are supported at this time.')
            return None

        if storeid:
            ms = self.get_msgstore(storeid)
        else:
            ms = self.get_def_msgstore()

        f  = BBContactsFolder(self, fname, ms)
        ms.add_folder(f)

        return f
Sriram Karra's avatar
Sriram Karra committed
460

461 462 463
    @classmethod
    def new_store (self, fname, ftype=None):
        """See the documentation in class PIMDB.
Sriram Karra's avatar
Sriram Karra committed
464 465 466
        fname should be a filename in this case.
        """

467 468 469 470
        ## FIXME: This routine should really be one of the cases in the
        ## constructor. 

        with codecs.open(fname, 'w', encoding='utf-8') as bbf:
Sriram Karra's avatar
Sriram Karra committed
471 472 473 474 475
            bbf.write(';; -*-coding: utf-8-emacs;-*-\n')
            bbf.write(';;; file-format: 7\n')
            bbf.close()

        logging.info('Successfully Created BBDB file: %s', fname)
476 477 478 479 480 481

        # # The following can be uncommented when this stuff is also put into
        # # the constructor

        # ms = MessageStore(self, fname)
        # self.add_msgstore(ms)
Sriram Karra's avatar
Sriram Karra committed
482 483 484 485

    def show_folder (self, gid):
        logging.info('%s: Not Implemented', 'pimd_bb:show_folder()')

486
    def del_folder (self, gid, store=None):
Sriram Karra's avatar
Sriram Karra committed
487 488
        """See the documentation in class PIMDB"""

489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
        if not store:
            logging.error('BBDB:del_folder() needs store to be specified.')
            return

        if not store in self.get_msgstores():
            logging.error('BBDB:del_folder() Could not locate store: %s', store)
            return

        st = self.get_msgstore(store)
        if not gid in st.get_folders():
            logging.error('BBDB:del_folder() Could not locate store: %s', store)
            return

        fold = st.get_folder(gid)
        st.remove_folder(fold)
        self.remove_folder(fold)
Sriram Karra's avatar
Sriram Karra committed
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524

    def set_folders (self):
        """See the documentation in class PIMDB"""

        for name, store in self.get_msgstores().iteritems():
            for name, f in store.get_folders().iteritems():
                self.add_to_folders(f)

    def set_def_folders (self):
        """See the documentation in class PIMDB"""

        def_store  = self.get_def_msgstore()
        def_folder = def_store.get_folder(MessageStore.get_def_folder_name())
        self.set_def_folder(Folder.CONTACT_t, def_folder)

    def set_sync_folders (self):
        """See the documentation in class PIMDB"""

        raise NotImplementedError

525 526 527 528 529 530 531 532
    def prep_for_sync (self, dbid, pname, is_dry_run=False):
        if is_dry_run:
            ## No backup for Dry Run
            logging.info('BBDB database not backed up for dry run')
            return

        ## Make a backup of the BBDB store into the backup directory
        conf = self.get_config()
533
        db1  = conf.get_profile_db1(pname)
534
        bdir = os.path.join(conf.get_user_dir(), conf.get_backup_dir())
535 536

        if not os.path.exists(bdir):
537
            logging.info('Creating BBDB backup directory at: %s', bdir)
538 539
            os.mkdir(bdir)

540 541 542 543 544 545 546
        period = conf.get_backup_hold_period()
        logging.info('Deleting BBDB backup files older than %d days, '
                     'if any...', period)
        utils.del_files_older_than(bdir, period)
        logging.info('Deleting BBDB backup files older than %d days, '
                     'if any...done', period)    

547
        stamp = string.replace(str(datetime.datetime.now()), ' ', '.')
548
        stamp = string.replace(stamp, ':', '-')
549
        backup_name = os.path.join(bdir, 'bbdb_backup.' + pname + '.' + stamp)
550 551 552 553 554 555

        if db1 == dbid:
            src = conf.get_stid2(pname)
        else:
            src = conf.get_stid1(pname)

556 557 558 559
        src = utils.abs_pathname(conf, src)

        logging.info('Backedup BBDB Store (%s) to file: %s', src, backup_name)
        shutil.copy2(src, backup_name)
Sriram Karra's avatar
Sriram Karra committed
560 561 562 563 564
      
    ##
    ## Now the non-abstract methods and internal methods
    ##

565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
    def _set_regexes_ver6 (self):
        res = {'string' : r'"[^"\\]*(?:\\.[^"\\]*)*"|nil',
               'ws'     : '\s*'}
        re_str_ar = 'nil|\(((' + res['string'] + ')' + res['ws'] + ')*\)'
        res.update({'string_array' : re_str_ar})

        ## Phones
        re_ph_vec = ('\[\s*((?P<phlabel>' + res['string'] + 
                     ')\s*(?P<number>(?P<unstructured>'  +
                     res['string'] + ')|'+
                     '(?P<structured>\d+\s+\d+\s+\d+\s+\d+)' +
                     '\s*))\]')
        re_phs = 'nil|(\(\s*(' + re_ph_vec + '\s*)+)\)'
        res.update({'ph_vec' : re_phs})

        ## Addresses
        re_ad_vec = ('\[\s*(?P<adlabel>' + res['string'] + ')\s*(' +
                     '(?P<streets>' + res['string_array'] + ')\s*' +
                     '(?P<city>'    + res['string'] + ')\s*' +
                     '(?P<state>'   + res['string'] + ')\s*' +
                     '(?P<zip>('    + res['string'] + ')|(' + '\d\d\d\d\d))\s*' +
                     '(?P<country>' + res['string'] + ')' +
                     ')\s*\]')
        re_ads = 'nil|\(\s*(' + re_ad_vec + '\s*)+\)'
        res.update({'ad_vec' : re_ads})


        re_note = ('\((?P<field>[^()]+)\s*\.\s*(?P<value>' +
                   res['string'] + '|\d+)+\)')
        re_notes = '\((' + re_note + '\s*)+\)'
        res.update({'note'  : re_note})
        res.update({'notes' : re_notes})

        ## A full contact entry
        re_con = ('\[\s*' +
                  '(?P<firstname>' + res['string']       + ')\s*' +
                  '(?P<lastname>'  + res['string']       + ')\s*' +
                  '(?P<aka>'       + res['string_array'] + ')\s*' +
                  '(?P<company>'   + res['string']       + ')\s*' +
                  '(?P<phones>'    + res['ph_vec']       + ')\s*' +
                  '(?P<addrs>'     + res['ad_vec']       + ')\s*' +
                  '(?P<emails>'    + res['string_array'] + ')\s*' +
                  '(?P<notes>'     + res['notes']        + ')\s*' +
                  '(?P<cache>'     + res['string']       + ')\s*' +
                  '\s*\]')
        
        ver = '6'

        ## Now save some of the regexes for later use...
        self.add_regexes(ver, {
            'con_re' : re_con,
            'str_re' : res['string'],
            'adr_re' : re_ad_vec,
            'ph_re'  : re_ph_vec,
            'note_re' : res['note'],
            'notes_re' : res['notes'],
            })

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    def _set_regexes_ver7 (self):
        res = {'string' : r'"[^"\\]*(?:\\.[^"\\]*)*"|nil',
               'ws'     : '\s*'}
        re_str_ar = 'nil|\(((' + res['string'] + ')' + res['ws'] + ')*\)'
        res.update({'string_array' : re_str_ar})

        ## Phones
        re_ph_vec = ('\[\s*((?P<phlabel>' + res['string'] + 
                     ')\s*(?P<number>(?P<unstructured>'  +
                     res['string'] + ')|'+
                     '(?P<structured>\d+\s+\d+\s+\d+\s+\d+)' +
                     '\s*))\]')
        re_phs = 'nil|(\(\s*(' + re_ph_vec + '\s*)+)\)'
        res.update({'ph_vec' : re_phs})

        ## Addresses
        re_ad_vec = ('\[\s*(?P<adlabel>' + res['string'] + ')\s*(' +
                     '(?P<streets>' + res['string_array'] + ')\s*' +
                     '(?P<city>'    + res['string'] + ')\s*' +
                     '(?P<state>'   + res['string'] + ')\s*' +
                     '(?P<zip>('    + res['string'] + ')|(' + '\d\d\d\d\d))\s*' +
                     '(?P<country>' + res['string'] + ')' +
                     ')\s*\]')
        re_ads = 'nil|\(\s*(' + re_ad_vec + '\s*)+\)'
        res.update({'ad_vec' : re_ads})


        re_note = ('\((?P<field>[^()]+)\s*\.\s*(?P<value>' +
                   res['string'] + '|\d+)+\)')
        re_notes = '\((' + re_note + '\s*)+\)'
        res.update({'note'  : re_note})
        res.update({'notes' : re_notes})

        ## A full contact entry
        re_con = ('\[\s*' +
                  '(?P<firstname>' + res['string']       + ')\s*' +
                  '(?P<lastname>'  + res['string']       + ')\s*' +
                  '(?P<affix>'     + res['string_array'] + ')\s*' +
                  '(?P<aka>'       + res['string_array'] + ')\s*' +
                  '(?P<company>'   + res['string_array'] + ')\s*' +
                  '(?P<phones>'    + res['ph_vec']       + ')\s*' +
                  '(?P<addrs>'     + res['ad_vec']       + ')\s*' +
                  '(?P<emails>'    + res['string_array'] + ')\s*' +
                  '(?P<notes>'     + res['notes']        + ')\s*' +
                  '(?P<cache>'     + res['string']       + ')\s*' +
                  '\s*\]')
        
        ver = '7'

        ## Now save some of the regexes for later use...
        self.add_regexes(ver, {
            'con_re' : re_con,
            'str_re' : res['string'],
            'adr_re' : re_ad_vec,
            'ph_re'  : re_ph_vec,
            'note_re' : res['note'],
            'notes_re' : res['notes'],
            })

    ##
    ## Now the non-abstract methods and internal methods
    ##

686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
    @classmethod
    def get_bbdb_time (self, t=None):
       """Convert a datetime.datetime object to a time string formatted in the
       bbdb-time-stamp-format of version 7 file format. BBDB timestamps are
       always represented in UTC. So the passed value should either be a naive
       object having the UTC time, or an aware object with tzinfo set."""
    
       # The bbbd ver 7 format uses time stamps in the following format:
       # "%Y-%m-%d %T %z", for e.g. 2012-04-17 09:49:16 +0000. The following
       # code converts a specified time instance (seconds since epoch) to the
       # right format
    
       if not t:
           t = datetime.datetime.utcnow()
       else:
           if t.tzinfo:
               t = t - t.tzinfo.utcoffset(t)
    
       return t.strftime('%Y-%m-%d %H:%M:%S +0000', )

    @classmethod
    def parse_bbdb_time (self, t):
        """Return a datetime object containing naive UTC timestamp based on
        the specified BBDB timestamp string."""

       # IMP: Note that we assume the time is in UTC - and ignore what is
       # actually in the string. This sucks, but this is all I am willing to
       # do for the m moment. FIXME

        res = re.search(r'(\d\d\d\d\-\d\d\-\d\d \d\d:\d\d:\d\d).*', t)
        if res:
            t = res.group(1)
        else:
            return None
        
        return datetime.datetime.strptime(t, '%Y-%m-%d %H:%M:%S')