"""Util functions related to email functionalities.""" import base64 import hashlib import json import os import smtplib import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from segment import analytics from notifications_delivery import config from notifications_delivery.constants.notifications import ( ANALYTICS_DIGEST_STORES, SEGMENT_IDENTIFY_URL, SEGMENT_TRACKING_URL, TOKEN_SECRET, TRENDING_TRACKS_REGION_MAPPING, ) from notifications_delivery.utils import template_renderer from notifications_delivery.utils.translations import get_translations analytics.write_key = config.SEGMENT_WRITE_KEY def format_notification_for_rendering(notification): """Format a notification for rendering depending on the template. Args: notification (dict): the notification to format. Returns: None """ if notification['template'] == 'analytics_digest': notification['payload']['stores'] = ANALYTICS_DIGEST_STORES if 'top_new_releases' in notification['payload'] and \ notification['payload']['top_new_releases']: populate_missing_stores( notification['payload']['top_new_releases']) if 'top_tracks' in notification['payload'] and \ notification['payload']['top_tracks']: populate_missing_stores( notification['payload']['top_tracks']) if 'top_releases' in notification['payload'] and \ notification['payload']['top_releases']: populate_missing_stores( notification['payload']['top_releases']) if notification['template'] == 'auth0_sso': # create a url with user data, so workstation can link them. user = notification['users_info'][0] params = { 'user_id': user.get('user_id'), 'auth0_id': user.get('auth0_id'), 'when': time.time() } data = base64.b64encode(json.dumps(params).encode()).decode('utf-8') url = 'https://{}/login/single-signon?token={}'.format( config.WORKSTATION_HOST, data) notification['payload']['signin_url'] = url notification['payload']['user_email'] = user.get('email') if notification['template'] == 'trending_tracks': region = TRENDING_TRACKS_REGION_MAPPING.get( notification['payload']['country_group_name'], {}) notification['payload']['region_label'] = region.get( 'label', notification['payload']['country_group_name']) notification['payload']['region_emoji'] = region.get('emoji', '') if notification['template'] in [ 'video_product_rejection', 'video_product_approval']: project_id = notification['payload'].get('project_id', '') product_id = notification['payload'].get('product_id', '') if config.ENVIRONMENT == config.ENVIRONMENT_PROD: base_url = 'https://workstation.theorchard.com' else: base_url = 'https://workstation.qaorch.com' product_url = '{}/project/{}/product/{}/video'.format( base_url, project_id, product_id) notification['payload']['product_url'] = product_url if notification['template'] == 'release_rejection': project_id = notification['payload'].get('project_id', '') product_id = notification['payload'].get('product_id', '') rejection_reasons = notification['payload'].get('rejection_reasons', '') if config.ENVIRONMENT == config.ENVIRONMENT_PROD: base_url = 'https://workstation.theorchard.com' else: base_url = 'https://workstation.qaorch.com' product_url = '{}/project/{}/product/{}/overview{}'.format( base_url, project_id, product_id, rejection_reasons) notification['payload']['product_url'] = product_url if notification['template'] in [ 'digital_rejection', 'digital_approval', 'branded_analytics_digest', 'branded_spike_detector', ]: project_id = notification['payload'].get('project_id', '') product_id = notification['payload'].get('product_id', '') default_brand = notification['payload'].get('default_brand', '') rejection_reasons = notification['payload'].get('rejection_reasons', []) if default_brand == 'awal': prod_url = 'https://workstation.awal.com' qa_url = 'https://workstation.qaawal.com' else: prod_url = 'https://workstation.theorchard.com' qa_url = 'https://workstation.qaorch.com' if config.ENVIRONMENT == config.ENVIRONMENT_PROD: base_url = prod_url else: base_url = qa_url product_url = '{}/project/{}/product/{}/overview'.format( base_url, project_id, product_id) notification['payload']['product_url'] = product_url if default_brand == 'awal': pref_prod_url = 'https://settings.awal.com' pref_qa_url = 'https://settings.qaawal.com' else: pref_prod_url = 'https://settings.theorchard.com' pref_qa_url = 'https://settings.qaorch.com' if config.ENVIRONMENT == config.ENVIRONMENT_PROD: pref_base_url = pref_prod_url else: pref_base_url = pref_qa_url if rejection_reasons: rejection_reasons = [ { 'title': reason.get('title', ''), 'comments': reason.get('comments', '').replace('\n', '
') } for reason in rejection_reasons ] notification['payload']['rejection_reasons'] = rejection_reasons notification['payload']['pref_base_url'] = pref_base_url def populate_missing_stores(items): """Populate missing stores with N/A. Args: items ([dict]): the list of items containing the stores. Returns: None """ for store in ANALYTICS_DIGEST_STORES: for item in items: if store not in item['store_percentages']: item['store_percentages'][store] = 'N/A' def get_email_subject(template, payload, user_locale): """Get the email subject based on the template. Args: template (str): the template name. payload (dict): the notification payload. user_locale (str): the user's locale (e.g. "en"). Returns: str: the email subject. """ _ = get_translations(user_locale).gettext if config.ENVIRONMENT != config.ENVIRONMENT_PROD: env = '[{0}] '.format(config.ENVIRONMENT) else: env = '' if template == 'analytics_digest': start_date = template_renderer.datetimeformat( payload['start_date'], date_format='%b %-d') end_date = template_renderer.datetimeformat( payload['end_date'], date_format='%b %-d') # account_name is a mandatory field, but just in case we don't have it for some reason account_name = payload.get('account_name') if account_name: return _( '{env}Weekly Analytics Digest {start_date} - {end_date} for {account_name}').format( env=env, start_date=start_date, end_date=end_date, account_name=account_name) else: return _( '{env}Weekly Analytics Digest {start_date} - {end_date}').format( env=env, start_date=start_date, end_date=end_date) if template == 'spike_detector': date = template_renderer.datetimeformat( payload['date'], date_format='%B %-d') track_number = len(payload['trending_tracks']) if track_number < 2: return _( '{env}Spike Detection: {track_number} Track on {date} 📈').\ format(env=env, track_number=track_number, date=date) return _( '{env}Spike Detection: {track_number} Tracks on {date} 📈').format( env=env, track_number=track_number, date=date) if template == 'auth0_sso': return _('{env}Activate Single Sign On Workstation Experience').format( env=env) if template == 'trending_tracks': group_name = payload['country_group_name'] return _( '{env}The Orchard - This Week’s Trending Tracks in {group_name}').\ format(env=env, group_name=group_name) if template == 'video_product_rejection': return _('{env}Action Required: Your Video Has Been Rejected').format( env=env) if template == 'video_product_approval': return _('{env}Your Video Has Been Approved').format( env=env) if template == 'release_approval': if len(payload['approved_releases']) > 1: return _('{env}Your products have been approved.').format( env=env) return _('{env}Your product has been approved.').format( env=env) if template == 'release_rejection': return _( "{env}Action Needed: We've encountered an issue with one of your releases." ).format(env=env) if template == 'digital_approval': product_name = payload['product_name'] artist_name = payload['artist_name'] subject_text = '{env}Congratulations! {product_name} by {artist_name} has been approved.' if payload['default_brand'] == 'orchard': subject_text = '{env}{product_name} by {artist_name} has been approved' return _(subject_text).format( env=env, product_name=product_name, artist_name=artist_name) if template == 'digital_rejection': product_name = payload['product_name'] artist_name = payload['artist_name'] return _( '{env}We’ve encountered one or more issues with {product_name} by {artist_name}' ).format(env=env, product_name=product_name, artist_name=artist_name) def get_preview_text(template, payload, user_locale): """Get the email preview text based on the template. Args: template (str): the template name. payload (dict): the notification payload. user_locale (str): the user's locale (e.g. "en"). Returns: str: the preview text. """ _ = get_translations(user_locale).gettext if template == 'analytics_digest': start_date = template_renderer.datetimeformat( payload['start_date'], date_format='%b %-d') end_date = template_renderer.datetimeformat( payload['end_date'], date_format='%b %-d') return _( 'Weekly performance from {start_date} - {end_date}').format( start_date=start_date, end_date=end_date) if template == 'spike_detector' or template == 'trending_tracks': trending_tracks = payload['trending_tracks'] if not trending_tracks: return '' track_name = trending_tracks[0]['track_name'] if len(trending_tracks) < 2: return _( '{track_name} is trending').format(track_name=track_name) elif len(trending_tracks) == 2: return _( '{track_name} + 1 other track are trending').format( track_name=track_name) else: return _( '{track_name} + {num_tracks} other tracks are trending').\ format( track_name=track_name, num_tracks=len(trending_tracks) - 1) if template == 'video_product_rejection': return _('Action Required: Your Video Has Been Rejected') if template == 'video_product_approval': return _('Your Video Has Been Approved') if template == 'digital_approval': return _('Your Digital Product Has Been Approved') if template == 'digital_rejection': return _('Your Digital Product Has Been Rejected') if template == 'release_approval': if len(payload['approved_releases']) > 1: return _('Products Approved') return _('Product Approved') if template == 'release_rejection': return _("Action Needed: We've encountered an issue with one of your releases") return '' def generate_unsubscribe_url(user_id, feed_name, feed_id): """Generate an unsubscribe URL. Args: user_id (str): the orchard user ID. feed_name (str): the feed name. feed_id (str): the feed ID. Returns: str: the unsubscribe URL containing the sha1 hashed token. """ generated_hash = hashlib.sha1() generated_hash.update(user_id.encode('utf-8')) generated_hash.update(feed_name.encode('utf-8')) generated_hash.update(feed_id.encode('utf-8')) generated_hash.update(TOKEN_SECRET.encode('utf-8')) generated_token = generated_hash.hexdigest() url = '{0}/unsubscribe'.format(config.BFF_NOTIFICATIONS_URL) url += '?user_id={0}'.format(user_id) url += '&feed_name={0}'.format(feed_name) url += '&feed_id={0}'.format(feed_id) url += '&key={0}'.format(generated_token) return url def generate_identify_url(user_info): """Generate a segment identify URL. Args: user_info (dict): the user info. Returns: str: the identify URL. """ params = { 'writeKey': config.SEGMENT_WRITE_KEY, 'userId': user_info.get('user_id'), 'traits': { 'email': user_info.get('email'), 'first_name': user_info.get('first_name'), 'last_name': user_info.get('last_name'), 'vendor_id': user_info.get('account', {}).get('vendor_id'), 'subaccount_id': user_info.get('account', {}).get('subaccount_id'), 'user_id': user_info.get('user_id') } } data = base64.b64encode(json.dumps(params).encode()).decode('utf-8') return '{0}?data={1}'.format(SEGMENT_IDENTIFY_URL, data) def generate_generic_tracking_url(user_id, subject): """Generate a segment tracking URL. Args: user_id (str): the orchard user ID. email_subject (str): the email subject. Returns: str: the tracking URL. """ params = { 'writeKey': config.SEGMENT_WRITE_KEY, 'userId': user_id, 'event': 'Email Opened', 'properties': { 'subject': subject, 'userId': user_id } } data = base64.b64encode(json.dumps(params).encode()).decode('utf-8') return '{0}?data={1}'.format(SEGMENT_TRACKING_URL, data) def generate_tracking_url(user_id, feed_name, feed_id, email_subject): """Generate a segment tracking URL. Args: user_id (str): the orchard user ID. feed_name (str): the feed name. feed_id (str): the feed ID. email_subject (str): the email subject. Returns: str: the tracking URL. """ params = { 'writeKey': config.SEGMENT_WRITE_KEY, 'userId': user_id, 'event': 'Email Opened', 'properties': { 'subject': email_subject, 'feed_name': feed_name, 'feed_id': feed_id, 'userId': user_id } } data = base64.b64encode(json.dumps(params).encode()).decode('utf-8') return '{0}?data={1}'.format(SEGMENT_TRACKING_URL, data) def track_email_sent(user_id, feed_name, feed_id): """Track email sent. Args: user_id (str): the orchard user ID. feed_name (str): the feed name. feed_id (str): the feed ID. Returns: None """ analytics.identify(user_id) analytics.track(user_id, 'Email Delivered', { 'feed_name': feed_name, 'feed_id': feed_id, 'userId': user_id }) def track_generic_email_sent(user_id: str, subject: str) -> None: """Track generic email sent. Args: user_id (str): the orchard user ID. subject (str): the email subject. Returns: None """ analytics.identify(user_id) analytics.track(user_id, 'Email Delivered', { 'subject': subject, 'userId': user_id }) def track_email_bounce(email): """Track email bounce. Args: email (str): the orchard user email. Returns: None """ analytics.track(email, 'Email Bounced', {'email': email}) def track_email_complaint(email): """Track email complaint. Args: email (str): the orchard user email. Returns: None """ analytics.track(email, 'Email Complained', {'email': email}) def extract_bounced_emails_from_ses_notification(message): """Extract bounced ses notification. Args: message (dict): message body pulled from SQS-queue. Returns: list: List of emails. """ message = json.loads(message['Message']) recipients = message['bounce']['bouncedRecipients'] emails = [] for r in recipients: emails.append(r['emailAddress']) return emails def extract_complaint_emails_from_ses_notification(message): """Extract complaint ses notification. Args: message (dict): message body pulled from SQS-queue. Returns: list: List of emails. """ message = json.loads(message['Message']) recipients = message['complaint']['complainedRecipients'] emails = [] for r in recipients: emails.append(r['emailAddress']) return emails def generate_html_file(template_name, html_content, output_dir_path=None): """Generate output email file into the project root (or custom) folder. Args: template_name (str): name of the template html_content (str): contents of the html file output_dir_path (str): path to the output directory Returns: None """ try: if output_dir_path and not os.path.exists(output_dir_path): raise FileNotFoundError('Output directory does not exist') if output_dir_path: dir_path = output_dir_path else: abs_path = os.path.abspath(os.path.dirname(__file__)) dir_path = os.path.join(abs_path, '../../') full_file_path = os.path.join(dir_path, f'{template_name}.html') with open(full_file_path, 'w') as file: file.write(html_content) except FileNotFoundError as e: print('Error creating email HTML file:', str(e)) raise def send_smtp_email(subject, html_content): """Send an email via SMTP server. Intended only for dev purposes until we figure out a better way to debug all of this. Args: html_content (str): the html content of the email. Returns: bool: True if email was sent successfully, False otherwise. """ # Throws an exception if the environment is not dev if config.ENVIRONMENT != config.ENVIRONMENT_DEV: raise Exception('Cannot send SMTP emails in production environment') # SMTP server details smtp_server = os.environ.get('TEST_SMTP_SERVER_HOST') smtp_port = int(os.environ.get('TEST_SMTP_SERVER_PORT')) # Email details sender_email = os.environ.get('TEST_SMTP_SENDER_ADDRESS') receiver_email = os.environ.get('TEST_SMTP_RECEIVER_ADDRESS') # SMTP authentication credentials username = os.environ.get('TEST_SMTP_SENDER_USERNAME') password = os.environ.get('TEST_SMTP_SENDER_PASSWORD') # Create the email message msg = MIMEMultipart() msg['From'] = sender_email msg['To'] = receiver_email msg['Subject'] = subject # Attach the message to the email msg.attach(MIMEText(html_content, 'html')) try: # Connect to the SMTP server and start TLS server = smtplib.SMTP(smtp_server, smtp_port) server.starttls() server.login(username, password) server.sendmail(sender_email, receiver_email, msg.as_string()) print('Email sent successfully!') return True except smtplib.SMTPException as e: print('Error sending email:', str(e)) return False finally: server.close()