1    #!/usr/bin/env python
       2    import os, re, time, string, cPickle, tempfile, fileinput
       3    
       4    # Send an email out to everyone about a new photo album on Yahoo! photos.
       5    #
       6    # For each set of photographs:
       7    #
       8    # FIRST EXECUTION OF SCRIPT:
       9    #   Everyone gets an email.
      10    #
      11    # SUBSEQUENT EXECUTIONS:
      12    #   New email addresses added to distribution list (since first
      13    #   execution) will get an email; people who already got an email
      14    #   as a result of a prior execution will NOT get another one.
      15    # .....................................................................
      16    #
      17    # 1st version: Sun Apr  1, 2001
      18    # .....................................................................
      19    
      20    info = {}
      21    info['test'] = 't'
      22    info['test'] = None
      23    
      24    # Message:
      25    info['mirror_message'] = 'message'
      26    info[info['mirror_message']] = """Hello,
            
            This album contains photos taken while working as a courier
            for a California labor union ( http://www.cops.cc/ ) in the
            greater Los Angeles area.  It covers the months of September
            and October whereas during January-August, while not
            working, I was able to do one roll per month.
            
                  ROLL:  __roll__
                  DATE:  __date__
                  DESC:  __description__
              
                   URI:  __URI__
            
            ............................................................
              If you do not want to receive these emails, write:
              cymbaLa@Lafn.org
            
              Know someone who might like to get these photo emails?
              ...send their address.  Is there something in California
              you'd like to see in a future album?  ...send your idea!
            ............................................................"""
    
      48    
      49    # Roll info:
      50    dict = {
      51        -1:
      52        {       'roll': '__roll__',
      53                'date': '__date__',
      54         'description': '__description__',
      55                 'URI': '__URI__'},
      56        8:
      57        {       'roll': 'paradise',
      58                'date': 'October 2001',
      59         'description': 'Los Angeles October',
      60                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      61        7:
      62        {       'roll': 'giraffes',
      63                'date': 'August 2001',
      64         'description': 'Los Angeles August',
      65                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      66        6:
      67        {       'roll': 'peerless',
      68                'date': 'July 2001',
      69         'description': 'Mister Chipy and Friends',
      70                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      71        5:
      72        {       'roll': 'brothers',
      73                'date': 'June 2001',
      74         'description': 'LA Streets and X-Country Drive',
      75                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      76        4:
      77        {       'roll': 'romero',
      78                'date': 'May 2001',
      79         'description': 'Mission District & Coit Tower Murals',
      80                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      81        3:
      82        {       'roll': 'slopes',
      83                'date': 'April 2001',
      84         'description': 'Flowery Slopes & LA Scenes',
      85                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      86        2:
      87        {       'roll': 'pipes',
      88                'date': 'March 2001',
      89         'description': 'Arizona Wildflowers',
      90                 'URI': 'http://www.lafn.org/~cymbala/photos.html'},
      91        1:
      92        {       'roll': 'pantry',
      93                'date': 'February 2001',
      94         'description': 'LA Streets and Arizona Wildflowers',
      95                 'URI': 'http://photos.yahoo.com/rcymbala'},
      96        0:
      97        {       'roll': '__roll__',
      98                'date': '__date__',
      99         'description': '__description__',
     100                 'URI': '__URI__'}
     101        }
     102    #
     103    info['data'] = dict
     104    
     105    # Subject:
     106    info['header_subject_uppercase'] = 'Photo ALBUM:'
     107    
     108    # Time:
     109    info['time'] = time.strftime(
     110        "%Y%m%d.%H%M.%S", time.localtime(time.time()))
     111    
     112    # Database:
     113    info['database_pathfile'] = os.path.expanduser(
     114        '~/Corresp/photos.cpickle')
     115    
     116    # Address book:
     117    info['addresses_pathfile_test'] = os.path.expanduser(
     118        '~/Corresp/photos_test.email')
     119    info['addresses_pathfile'] = os.path.expanduser(
     120        '~/Corresp/photos.email')
     121    
     122    # NOTE:
     123    # This assumes the alias `me' (~/.mailrc: alias me cymbaLa@Lafn.org)
     124    # which is used as recipient; everyone else gets a blind-carbon-copy.
     125    
     126    class Program:
     127    
     128        def __init__(self):
     129            #
     130            self.MAIL_COMMAND = 'mail'
     131            self.tempfile_pathfile = tempfile.mktemp()
     132            pass
     133    
     134        def __call__(self, info):
     135    
     136            # ---
     137            # Changes to information.
     138            # Creates self.info and parses address book.
     139            self.info_change(info)
     140            info = ''
     141            
     142            # ---
     143            # Check files (do after `self.info_change')
     144            self.check_files()
     145    
     146            # ---
     147            # Get pickle
     148            self.my_cPickle('get')
     149    
     150            # ---
     151            # This will erase record of previous sents/sends.
     152            # USE WITH CAUTION.
     153            # The next line can result in duplicates being sent.
     154            # ## self.pickle['data'][self.info['roll_number_max']]['sent'] = {}
     155    
     156            # ---
     157            # These will prevent sending and show useful information.
     158            # ## raise str(self.pickle)
     159            # ## raise str(self.info['addresses'].keys())
     160            
     161            # ---
     162            # Send emails
     163            self.send_emails(info)
     164    
     165            # ---
     166            # Store database in a pickle.
     167            self.pickle_marinate()
     168            self.my_cPickle('put')
     169            
     170            pass
     171    
     172        def send_emails(self, info):
     173            #
     174            # Initialize
     175            sent = {}
     176            if self.info['roll_number_max'] in self.pickle[
     177                'data'].keys():
     178                #
     179                if 'sent' in self.pickle[
     180                    'data'][self.info['roll_number_max']].keys():
     181                    #
     182                    sent = self.pickle[
     183                        'data'][self.info['roll_number_max']]['sent']
     184                    pass
     185                pass
     186    
     187            # Add addresses.
     188            for address in self.info['addresses'].keys():
     189                if not address in sent.keys():
     190                    #
     191                    # address is all UPPERCASE letters.
     192                    sent[address] = {
     193                        'address': self.info['addresses'][address],
     194                        'flag': None}
     195                pass
     196    
     197            # Send if flag is not None...
     198            
     199            # ``man mail''
     200            bcc_addr = []
     201            
     202            for key in sent.keys():
     203                if (sent[key]['flag'] == None or \
                    not self.info['test'] == None):
     205                    address = sent[key]['address']
     206                    spam = 'Sending...'
     207                    print spam, key
     208                    print ' ' * len(spam), address
     209    
     210                    # List of addresses to receive email
     211                    bcc_addr.append(address)
     212    
     213                    # This prevents sending another copy if script run again
     214                    sent[key]['flag'] = self.info['time']
     215                    
     216                    pass
     217                pass
     218    
     219            # Save information across executions
     220            self.info['data'][
     221                self.info['roll_number_max']][
     222                'sent'] = sent
     223    
     224            if len(bcc_addr) > 0:
     225                #
     226                # ``man mail''
     227                booleybooley = self.MAIL_COMMAND
     228    
     229                # Subject
     230                subject = string.join(
     231                    (self.info['header_subject_uppercase'],
     232                     self.info['data'][
     233                    self.info['roll_number_max']][
     234                    'description']))
     235                subject = string.join(('', subject, ''), '"')
     236                #
     237                booleybooley = string.join(
     238                    (booleybooley,
     239                     string.join(('-s',
     240                                  subject),
     241                                 ' ')),
     242                    ' ')
     243                
     244                # Send blind carbon copies to list
     245                booleybooley = string.join(
     246                    (booleybooley,
     247                     string.join(('-b',
     248                                 string.join(bcc_addr, ',')),
     249                                 ' ')),
     250                    ' ')
     251    
     252                # Send email to me
     253                booleybooley = string.join(
     254                    (booleybooley,
     255                     'me'),
     256                    ' ')
     257    
     258                # Store message to file to prepare for "cat".
     259                file = open(self.tempfile_pathfile, 'w')
     260                file.write(self.info[self.info['mirror_message']])
     261                file.close()
     262                
     263                booleybooley = string.join(
     264                    ('cat ' + self.tempfile_pathfile,
     265                     booleybooley),
     266                    ' | ')
     267                
     268                # raise booleybooley
     269                #
     270                # EXAMPLE:
     271                # cat /tmp/@10262.1 | mail -s "PHOTO GALLERY: LA Streets
     272                # and Arizona Wildflowers" -b
     273                # cymbala@juno.com,rcymbala@yahoo.com,cymbaLa@Lafn.org me
     274    
     275                # Hello, world!
     276                booleybooley_rc = os.system(booleybooley)
     277    
     278                if not booleybooley_rc == 0:
     279                    raise 'Failed:', booleybooley
     280                
     281                pass
     282            pass
     283    
     284        def pickle_marinate(self):
     285            #
     286            # Do pickle house-cleaning before storing pickle
     287    
     288            # Keep copy of message
     289            self.info['data'][
     290                self.info[
     291                'roll_number_max']][
     292                'message'] = self.info['message']
     293            del self.info['message']
     294    
     295            # Delete every key except 'data' and disk location of pickle
     296            for key in self.info.keys():
     297                if not (
     298                    key == 'data' or
     299                    key == 'database_pathfile'
     300                    ):
     301                    del self.info[key]
     302                    pass
     303                pass
     304            pass
     305    
     306        def my_cPickle(self, action):
     307            #
     308            if action == 'get':
     309                file = open(self.info['database_pathfile'], 'r')
     310                u = cPickle.Unpickler(file)
     311                
     312                # !!!!!
     313                self.pickle = u.load()
     314    
     315                file.close()
     316                pass
     317            elif action == 'put':
     318                file = open(self.info['database_pathfile'], 'w')
     319                p = cPickle.Pickler(file)
     320                
     321                # !!!!!
     322                p.dump(self.info)
     323    
     324                file.close()
     325                pass
     326            else:
     327                raise 'Action not recognized: ' + action
     328            pass
     329    
     330        def info_change(self, info):
     331            #
     332            if not info['test'] == None:
     333                info['addresses_pathfile'] = info[
     334                    'addresses_pathfile_test']
     335                pass
     336    
     337            # Find highest roll number.
     338            info['roll_number_max'] = 0
     339            for number in info['data'].keys():
     340                if number > info['roll_number_max']:
     341                    info['roll_number_max'] = number
     342                    pass
     343                pass
     344    
     345            # Get email addresses
     346            info = self.parse_address_book(info)
     347    
     348            # Replace __placeholders__ with meaningful information.
     349            info = self.replace_placeholders(info)
     350    
     351            # Move information to an attribute of self (original
     352            #  information is deleted after calling this function).
     353            self.info = info
     354            pass
     355    
     356        def replace_placeholders(self, info):
     357            key = info['mirror_message']
     358            message = info[key]
     359    
     360            locations = {}
     361            message_length = len(message)
     362            
     363            for i in range(message_length):
     364                char0 = message[i]
     365    
     366                if (i + 1) < message_length:
     367                    char1 = message[i+1]
     368                    pass
     369                else:
     370                    char1 = None
     371                    pass
     372                
     373                if char0 == '_' and char1 == '_':
     374                    #
     375                    # Is it an opening "__" or a closing "__"?
     376                    stop = i + 50
     377                    if stop > message_length:
     378                        stop = message_length
     379                        pass
     380                    pieces = string.split(message[i:stop], '__')
     381    
     382                    key = pieces[1]
     383                    #
     384                    # Look for key in info:
     385                    if key in info[
     386                        'data'][
     387                        info[
     388                        'roll_number_max']].keys(
     389                        ):
     390                        
     391                        locations[key] = [i,
     392                                          i + \
                                          len('__' * 2) + \
                                          len(key)]
     395                        pass
     396                    pass
     397                pass
     398            info['locations'] = locations
     399    
     400            # Do replacements
     401            for key in locations.keys():
     402    
     403                replacement = info['data'][
     404                    info[
     405                    'roll_number_max']][
     406                    key]
     407    
     408                start = locations[key][0]
     409                stop  = locations[key][1]
     410                placeholder = message[start:stop]
     411    
     412                # Replace
     413                message = message[:start] + replacement + message[stop:]
     414                locations[key] = (-1, -1)
     415    
     416                # Adjust locations if necessary
     417                for wooley in locations.keys():
     418                    if locations[wooley][0] > start:
     419                        adjustment = len(replacement) - len(placeholder)
     420                        locations[wooley][0] = locations[wooley][0] + adjustment
     421                        locations[wooley][1] = locations[wooley][1] + adjustment
     422                        pass
     423                    pass
     424                pass
     425    
     426            # Same as what happened at beginning of function, only reversed.
     427            key = info['mirror_message']
     428            info[key] = message
     429    
     430            return info
     431    
     432        def parse_address_book(self, info):
     433            #
     434            # alias cymbala-r_juno cymbala@juno.com
     435            # alias cymbala-r_lafn cymbaLa@Lafn.org
     436            # alias cymbala-r_yahoo rcymbala@yahoo.com
     437            # alias me cymbala-r_lafn
     438            #
     439            re_alias = re.compile(r'^[\s]*alias', re.I)
     440    
     441            addresses = {}
     442            aliases = {}
     443            for line in fileinput.input(info['addresses_pathfile']):
     444                if re_alias.match(line):
     445                    # Delete trailing newline
     446                    line = line[:-1]
     447                    pieces = string.split(line)
     448                    alias_name = pieces[1]
     449                    alias_name_upper = string.upper(alias_name)
     450                    email_address = pieces[2]
     451                    email_address_upper = string.upper(email_address)
     452                    #
     453                    aliases[alias_name_upper] = email_address_upper
     454                    addresses[email_address_upper] = email_address
     455                pass
     456    
     457            # If no ``@'' attempt to resolve address using other aliases.
     458            # Assumes, in general, that aliases don't have ``@'' in alias name.
     459            last_key_missing_at = None
     460            last_key_missing_count = 0
     461            flipper = 't'
     462            while flipper:
     463                flipper = None
     464                for key_upper_a in addresses.keys():
     465                    if string.find(addresses[key_upper_a], '@') == -1:
     466                        flipper = 't'
     467                        for key_upper_b in aliases.keys():
     468                            if key_upper_b == key_upper_a:
     469                                #
     470                                # Remove resolved alias, that's it, nothing else.
     471                                del addresses[key_upper_a]
     472                                pass
     473                            pass
     474                        
     475                        if key_upper_a == last_key_missing_at:
     476                            last_key_missing_count = last_key_missing_count + 1
     477                            if last_key_missing_count > 5:
     478                                raise 'Cannot resolve alias: ' + key_upper_a
     479                            pass
     480                        last_key_missing_at = key_upper_a
     481                        pass
     482                    pass
     483                pass
     484    
     485            info['addresses'] = addresses
     486            return info
     487    
     488        def check_files(self):
     489            #
     490            # Look for pathfiles.
     491            for key in self.info.keys():
     492                if key[-8:] == 'pathfile':
     493                    pathfile = self.info[key]
     494                    if not os.path.isfile(pathfile):
     495                        if pathfile[-7:] == 'cpickle':
     496                            #
     497                            # Create an empty pickle.
     498                            self.my_cPickle('put')
     499                            pass
     500                        else:
     501                            raise 'File not found: ' + pathfile
     502                    pass
     503                pass
     504            pass
     505        pass
     506    
     507    # MAIN:
     508    wooleywooley = Program()
     509    wooleywooley(info)
     510    
     511    ###
     512    #
     513    # Local variables:
     514    # py-indent-offset: 4
     515    # End: