""" Notifier - Email and Webhook Notifications Handles SMTP emails (Mailgun) and outgoing webhooks Compatible with Python 3.6+ """ import requests import logging import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from jinja2 import Template logger = logging.getLogger('Notifier') class Notifier: def __init__(self, config): self.config = config self.enabled = config['notifications']['enabled'] # SMTP configuration (preferred method) smtp_config = config['notifications'].get('smtp', {}) self.smtp_server = smtp_config.get('server') self.smtp_port = smtp_config.get('port', 587) self.smtp_user = smtp_config.get('user') self.smtp_password = smtp_config.get('password') self.sender_email = smtp_config.get('sender_email') self.recipients = config['notifications']['recipients'] self.webhook_config = config.get('webhooks', {}) def send_email(self, template_name, recipients, data, attachments=None): """ Send email via SMTP (Mailgun) Args: template_name: Name of email template recipients: List of email addresses or single email data: Template data dict attachments: List of file paths to attach (optional) """ if not self.enabled: logger.info("Notifications disabled, skipping email") return if not self.smtp_server or not self.smtp_user: logger.warning("SMTP not configured, skipping email") return try: # Simple templates (full template system would load from YAML) templates = { 'a1_to_a2_complete': { 'subject': "✅ Master Assets Downloaded - Campaign {campaign_name}", 'html': """

✅ Master Assets Downloaded Successfully

Campaign: {{ campaign_name }} ({{ campaign_number }})

Assets Downloaded: {{ asset_count }}

Status Updated: A1 → A2

Processed Assets:

{% for asset in processed_assets %}
{{ asset.asset_name }}

Tracking ID: {{ asset.tracking_id }}

Box File ID: {{ asset.box_file_id }}

Box URL: {{ asset.box_url }}

{% if asset.folder_path %}

DAM Path: {{ asset.folder_path }}

{% endif %}
{% endfor %}

✓ Complete: All assets downloaded from DAM and uploaded to Box with tracking IDs.

Campaign status updated from A1 to A2. Box Folder: 348304357505 (Local Adaptation)

""" }, 'a2_to_a3_complete': { 'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}", 'html': """

✅ Localized Assets Uploaded Successfully

Campaign: {{ campaign_name }}

Campaign ID: {{ campaign_id }}

Assets Uploaded: {{ asset_count }}

Status Updated: A2 → A3

✓ Complete: All localized assets have been uploaded to DAM.

Campaign status updated from A2 to A3.

""" }, 'a2_to_a3_batch_complete': { 'subject': "{% if failed_count == 0 %}✅{% else %}⚠️{% endif %} A2-A3 Batch Upload Complete - {{ successful_count }}/{{ total_files }} Successful", 'html': """

{% if failed_count == 0 %}✅ Batch Upload Complete{% else %}⚠️ Batch Upload Partial{% endif %}

Total Files: {{ total_files }}

✓ Successful: {{ successful_count }}

✗ Failed: {{ failed_count }}

Source Folder: {{ box_folder }}

{% if successful_files %}

✅ Successfully Uploaded ({{ successful_count }}):

{% for asset in successful_files %}
{{ asset.clean_filename }}

Original File: {{ asset.filename }}

Tracking ID: {{ asset.tracking_id }}

DAM Asset ID: {{ asset.asset_id }}

{% if asset.creativex_found %}

CreativeX Score: {{ asset.creativex_score }}

{% endif %}
{% endfor %} {% endif %} {% if failed_files %}

❌ Failed Uploads ({{ failed_count }}):

{% for asset in failed_files %}
{{ asset.filename }}

Error: {{ asset.error }}

{% if asset.tracking_id %}

Tracking ID: {{ asset.tracking_id }}

{% endif %}
{% endfor %} {% endif %}

Status: Batch processing complete.

""" }, 'upload_failed': { 'subject': "❌ Upload Failed - {filename}", 'html': """

❌ Upload Failed

Filename: {{ filename }}

Tracking ID: {{ tracking_id }}

Error Details

Error: {{ error }}

📌 Action Required: Please investigate the error and retry the upload.

""" }, 'a1_to_a2_partial': { 'subject': "⚠️ Partial Download - Campaign {campaign_name}", 'html': """

⚠️ Campaign Partially Processed

Campaign: {{ campaign_name }} ({{ campaign_number }})

Total Assets: {{ total_assets }}

Successful: {{ successful }} | Failed: {{ failed }}

{% if successful > 0 %}

✅ Successfully Processed ({{ successful }}):

{% for asset in processed_assets %}
{{ asset.asset_name }}

Tracking ID: {{ asset.tracking_id }}

Box File ID: {{ asset.box_file_id }}

Box URL: {{ asset.box_url }}

{% if asset.folder_path %}

DAM Path: {{ asset.folder_path }}

{% endif %}
{% endfor %} {% endif %} {% if failed > 0 %}

❌ Failed Assets ({{ failed }}):

{% for asset in failed_assets %}
{{ asset.asset_name }}

Error: {{ asset.error }}

{% endfor %} {% endif %}

⚠️ Status NOT updated. Campaign remains at A1.

The script will retry failed assets on the next run (every 5 minutes).

""" }, 'a2_to_a3_file_uploaded': { 'subject': "✅ Asset Uploaded to DAM - {clean_filename}", 'html': """

✅ Asset Uploaded Successfully (A2→A3)

Original Filename (from Box): {{ filename }}

Clean Filename (in DAM): {{ clean_filename }}

DAM Asset ID: {{ asset_id }}

Tracking ID: {{ tracking_id }}

Processing Details:

Master Asset ID: {{ master_asset_name }}

Uploaded to DAM Folder: {{ upload_folder }}

Downloaded from Box Folder: {{ box_folder }}

What Was Done:

{% if not creativex_found %}

⚠️ CreativeX Score Missing

No CreativeX score found in database for: {{ clean_filename }}

Default Values Used:

To add CreativeX score: Upload PDF report to Box folder 350605024645 and run creativex_scoring_storing.py

{% endif %}

Status: Asset processing complete.

Note: Campaign status will be updated to A3 once all assets are uploaded.

""" }, 'b1_to_b2_complete': { 'subject': "✅ Global Master Assets Downloaded - Campaign {campaign_name}", 'html': """

✅ Global Master Assets Downloaded Successfully

Campaign: {{ campaign_name }} ({{ campaign_number }})

Campaign Type: Global Masters

Assets Downloaded: {{ asset_count }}

Status Updated: B1 → B2

Processed Assets:

{% for asset in processed_assets %}
{{ asset.asset_name }}

Tracking ID: {{ asset.tracking_id }}

Box File ID: {{ asset.box_file_id }}

Box URL: {{ asset.box_url }}

{% if asset.folder_path %}

DAM Path: {{ asset.folder_path }}

{% endif %}
{% endfor %}

✓ Complete: All Global Master assets downloaded from DAM and uploaded to Box with tracking IDs.

Campaign status updated from B1 to B2. Box Folder: 349261192115 (Global Masters)

""" }, 'b1_to_b2_partial': { 'subject': "⚠️ Partial Download - Global Campaign {campaign_name}", 'html': """

⚠️ Global Campaign Partially Processed

Campaign: {{ campaign_name }} ({{ campaign_number }})

Campaign Type: Global Masters (B1→B2)

Total Assets: {{ total_assets }}

Successful: {{ successful }} | Failed: {{ failed }}

{% if successful > 0 %}

✅ Successfully Processed ({{ successful }}):

{% for asset in processed_assets %}
{{ asset.asset_name }}

Tracking ID: {{ asset.tracking_id }}

Box URL: {{ asset.box_url }}

{% if asset.folder_path %}

DAM Path: {{ asset.folder_path }}

{% endif %}
{% endfor %} {% endif %} {% if failed > 0 %}

❌ Failed Assets ({{ failed }}):

{% for asset in failed_assets %}
{{ asset.asset_name }}

Error: {{ asset.error }}

{% endfor %} {% endif %}

⚠️ Status NOT updated. Campaign remains at B1.

The script will retry failed assets on the next run (every 5 minutes).

""" }, 'a5_to_a6_rejections': { 'subject': "⚠️ NOT APPROVED Assets - Rework Required - Campaign {campaign_name}", 'html': """

⚠️ NOT APPROVED ASSETS - REWORK REQUIRED

⚠️ Campaign: {{ campaign_name }} ({{ campaign_number }})

NOT APPROVED assets: {{ rejected_count }} | Approved/skipped: {{ skipped_count }}

Status Updated: A5 → A6

{% for asset in rejected_assets %}
{{ asset.asset_name }}

Status: NOT APPROVED

Tracking ID: {{ asset.tracking_id }}{% if asset.is_existing %} (Updated existing){% endif %}

Box URL: {{ asset.box_url }}

{% if asset.folder_path %}

DAM Path: {{ asset.folder_path }}

{% endif %}
{# Rejection details commented out {% if asset.rejection_details %} {% set approver = asset.rejection_details.approver %} {% set legal = asset.rejection_details.legal %} {% set ia_cc = asset.rejection_details.ia_cc %} {% if approver.comment %}
🔴 APPROVER REJECTION

Reason: {{ approver.comment }}

{% if approver.certifier_name %}

Rejected By: {{ approver.certifier_name }}

{% endif %} {% if approver.date %}

Date: {{ approver.date }}

{% endif %}
{% endif %} {% if legal.comment %}
⚖️ LEGAL REJECTION

Reason: {{ legal.comment }}

{% if legal.certifier_name %}

Rejected By: {{ legal.certifier_name }}

{% endif %} {% if legal.date %}

Date: {{ legal.date }}

{% endif %}
{% endif %} {% if ia_cc.comment %}
🔍 IA&CC REJECTION

Reason: {{ ia_cc.comment }}

{% if ia_cc.certifier_name %}

Rejected By: {{ ia_cc.certifier_name }}

{% endif %} {% if ia_cc.date %}

Date: {{ ia_cc.date }}

{% endif %}
{% endif %} {% if not approver.comment and not legal.comment and not ia_cc.comment %}
(No specific rejection comments available)
{% endif %} {% else %}
(No rejection details available)
{% endif %} #}
{% endfor %}
📌 Next Steps: Please review and rework the above assets according to the rejection comments.

All rejected assets have been downloaded to Box Revisions folder (349441822875). Campaign status updated A5 → A6.

""" }, 'a5_to_a6_partial': { 'subject': "⚠️ Partial Rework Download - Campaign {campaign_name}", 'html': """

Rework Campaign Partially Processed (A5→A6)

Campaign: {{ campaign_name }} ({{ campaign_id }})

Campaign Number: {{ campaign_number }}

Total NOT APPROVED: {{ total_assets }}

Successful: {{ successful }}

Failed: {{ failed }}


{% if successful > 0 %}

✅ Successfully Processed ({{ successful }}):

{% endif %} {% if failed > 0 %}

❌ Failed Assets ({{ failed }}):

{% endif %}

Status NOT updated. Campaign remains at A5.

The script will retry failed assets on the next run (every 5 minutes).

""" }, 'a5_to_a6_no_rejections': { 'subject': "✅ No Rework Required - Campaign {campaign_name}", 'html': """

✅ No Rework Required

Campaign: {{ campaign_name }} ({{ campaign_number }})

Total assets checked: {{ total_assets }}

Approved/other status: {{ skipped_count }}

NOT APPROVED assets: 0

Good News!

All assets in the Final Assets folder are approved or have other status.

No assets with "NOT APPROVED" status were found, so no rework is required.

📌 Note:

A5→A6 script completed successfully with no rejected assets found.

""" }, 'a1_to_a2_no_assets': { 'subject': "⚠️ No Assets Found - Campaign {campaign_name}", 'html': """

⚠️ No Master Assets Found

Campaign: {{ campaign_name }} ({{ campaign_number }})

Campaign ID: {{ campaign_id }}

Status: A1

Campaign Set to A1 but No Assets Found

The Master Assets folder was searched (including subfolders) but no assets were found.

This campaign is set to status A1 but appears to have no master assets ready for download.

📌 What Happens Next:

A1→A2 script completed with no assets to process.

""" }, 'b1_to_b2_no_assets': { 'subject': "⚠️ No Assets Found - Global Campaign {campaign_name}", 'html': """

⚠️ No Global Master Assets Found

Campaign: {{ campaign_name }} ({{ campaign_number }})

Campaign ID: {{ campaign_id }}

Status: B1

Campaign Type: Global Masters

Campaign Set to B1 but No Assets Found

The Final Assets folder was searched (including subfolders) but no assets were found.

This Global Masters campaign is set to status B1 but appears to have no assets ready for download.

📌 What Happens Next:

B1→B2 script completed with no assets to process.

""" }, 'a4_webhook_sent': { 'subject': "📢 A4 Campaign Webhook Sent - {campaign_name}", 'html': """

📢 A4 Campaign Webhook Sent

Campaign: {{ campaign_name }} ({{ campaign_number }})

Status: A4 (Not Going Live)

Live Campaign: NO

Webhook Notification Sent

A webhook notification has been sent indicating this campaign is marked as A4 and will not go live.

Webhook URL: {{ webhook_url }}

Payload Sent:

A4 webhook monitor completed successfully.

""" }, 'daily_report': { 'subject': "📊 Ferrero Automation Daily Report - {report_date}", 'html': """

📊 Ferrero Automation Daily Report

{{ report_date }} - Generated at {{ report_time }}

📈 Overall Summary (Last 24 Hours)

Campaigns Found: {{ total_stats.campaigns_found }}
Campaigns Processed: {{ total_stats.campaigns_processed }}
✅ Completed: {{ total_stats.campaigns_completed }}
⚠️ Partial: {{ total_stats.campaigns_partial }}
No Assets: {{ total_stats.campaigns_no_assets }}
Total Assets: {{ total_stats.total_assets }}
✓ Successful: {{ total_stats.assets_successful }}
✗ Failed: {{ total_stats.assets_failed }}
{% if total_stats.assets_skipped > 0 %}
Skipped (Approved): {{ total_stats.assets_skipped }}
{% endif %} {% if total_stats.not_approved_count > 0 %}
🔴 NOT APPROVED: {{ total_stats.not_approved_count }}
{% endif %}
{% if total_stats.total_assets > 0 %}
Success Rate: {{ "%.1f"|format(total_stats.success_rate) }}%
{% endif %}

🔄 Workflow Breakdown

{% for workflow_name, stats in workflow_stats.items() %}
{{ workflow_name }}
{% if stats.campaigns_processed > 0 %}
Campaigns: {{ stats.campaigns_processed }}
Assets: {{ stats.total_assets }}
✓ Successful: {{ stats.assets_successful }}
✗ Failed: {{ stats.assets_failed }}
{% if stats.assets_skipped > 0 %}
Skipped: {{ stats.assets_skipped }}
{% endif %} {% if stats.not_approved_count > 0 %}
NOT APPROVED: {{ stats.not_approved_count }}
{% endif %}
{% if stats.campaign_details %}
View Campaign Details ({{ stats.campaign_details|length }})
{% for campaign in stats.campaign_details %}
{{ campaign.name }} ({{ campaign.number }})
Total: {{ campaign.total_assets }}, Success: {{ campaign.successful }}, Failed: {{ campaign.failed }}{% if campaign.get('skipped') %}, Skipped: {{ campaign.skipped }}{% endif %}
{% endfor %}
{% endif %} {% if stats.errors %}
⚠️ Errors ({{ stats.errors|length }})
{% for error in stats.errors[:10] %}
{{ error }}
{% endfor %} {% if stats.errors|length > 10 %}
... and {{ stats.errors|length - 10 }} more errors
{% endif %}
{% endif %} {% else %}

No activity in the last 24 hours

{% endif %}
{% endfor %}

📌 Automation Scripts: A1→A2, A2→A3, A5→A6, B1→B2

Scripts run every 5 minutes | Report generated daily at 7:00 PM

""" }, 'creativex_complete': { 'subject': "✅ CreativeX Scores Extracted - {file_count} files processed", 'html': """

✅ CreativeX Score Extraction Complete

Files Processed: {{ file_count }}

Scores Extracted: {{ success_count }}

Source: Box Folder 350605024645

Extracted Scores:

{% for score in processed_files %}
{{ score.filename }} {% if score.version_number > 1 %}Version {{ score.version_number }}{% if score.is_update %} (Updated){% endif %}{% endif %}

Quality Score: {{ score.quality_score }}

CreativeX ID: {{ score.creativex_id }}

{% if score.creativex_url %}

CreativeX URL: {{ score.creativex_url }}

{% endif %}

Box File ID: {{ score.box_file_id }}

{% if score.version_number > 1 %}

📝 Note: This is version {{ score.version_number }} of this file (previous versions preserved in database)

{% endif %}
{% endfor %}

✓ Complete: All scores extracted and stored in database.

Files Removed: Processed PDFs deleted from Box folder.

CreativeX scores stored with full JSON for future lookups.

""" }, 'creativex_partial': { 'subject': "⚠️ CreativeX Extraction Partial - {success_count}/{file_count} successful", 'html': """

⚠️ CreativeX Extraction Partially Complete

Total Files: {{ file_count }}

✓ Successful: {{ success_count }}

✗ Failed: {{ failed_count }}

Source: Box Folder 350605024645

{% if processed_files %}

✅ Successful Extractions ({{ success_count }}):

{% for score in processed_files %}
{{ score.filename }} - Score: {{ score.quality_score }} {% if score.version_number > 1 %} (Version {{ score.version_number }}){% endif %}
{% endfor %} {% endif %} {% if failed_files %}

❌ Failed Extractions ({{ failed_count }}):

{% for file in failed_files %}
{{ file.filename }}
Error: {{ file.error }}
{% endfor %} {% endif %}

⚠️ Action Required: Review failed extractions above.

Failed files remain in Box folder for retry.

Successful scores stored in database. Failed files not deleted from Box.

""" }, 'creativex_no_files': { 'subject': "ℹ️ CreativeX Extraction - No files found", 'html': """

ℹ️ CreativeX Extraction - No Files

Status: No PDF files found

Source: Box Folder 350605024645

Run Time: {{ timestamp }}

ℹ️ Note: This is expected behavior when no new PDFs are ready for processing.

Upload PDFs to Box folder 350605024645 to process CreativeX scores.

Script completed successfully with no errors.

""" }, 'a5_to_a6_no_rejections': { 'subject': "✅ No Rework Required - Campaign {campaign_name}", 'html': """
# ... (template content skipped for brevity)
""" } } except Exception as e: logger.error("Error creating templates: {}".format(str(e))) return try: # 1. Prepare message content template = templates.get(template_name) if not template: logger.error("Email template not found: {}".format(template_name)) return jinja_template = Template(template['html']) html_content = jinja_template.render(data) subject = template['subject'].format(**data) # 2. Create MIME message if attachments: # Use MIMEMultipart for attachments message = MIMEMultipart() message['From'] = self.sender_email message['To'] = ", ".join(recipients) if isinstance(recipients, list) else recipients message['Subject'] = subject # Attach HTML body message.attach(MIMEText(html_content, "html")) # Attach files from email.mime.base import MIMEBase from email import encoders import os for file_path in attachments: try: if os.path.exists(file_path): with open(file_path, "rb") as attachment: part = MIMEBase("application", "octet-stream") part.set_payload(attachment.read()) encoders.encode_base64(part) filename = os.path.basename(file_path) part.add_header( "Content-Disposition", f"attachment; filename= {filename}", ) message.attach(part) logger.info("Attached file: {}".format(filename)) else: logger.warning("Attachment not found: {}".format(file_path)) except Exception as e: logger.error("Failed to attach file {}: {}".format(file_path, str(e))) else: # Use standard MIMEText for simple emails message = MIMEText(html_content, "html") message['From'] = self.sender_email message['To'] = ", ".join(recipients) if isinstance(recipients, list) else recipients message['Subject'] = subject # 3. Send via SMTP with smtplib.SMTP(self.smtp_server, self.smtp_port) as server: server.starttls() server.login(self.smtp_user, self.smtp_password) server.send_message(message) logger.info("Email sent to {} (Template: {})".format(recipients, template_name)) except Exception as e: logger.error("Failed to send email: {}".format(str(e))) def send_webhook(self, url, payload): """ url: Webhook URL payload: dict to send as JSON Returns: bool: Success status """ try: # Get webhook config if exists webhook_config = None for name, config in self.webhook_config.items(): if config.get('url') == url: webhook_config = config break if not webhook_config: webhook_config = {'timeout_seconds': 10, 'auth': {}} # Prepare headers headers = {'Content-Type': 'application/json'} # Add auth if configured auth_config = webhook_config.get('auth', {}) if auth_config.get('type') == 'bearer' and auth_config.get('token'): headers['Authorization'] = 'Bearer {}'.format(auth_config['token']) elif auth_config.get('type') == 'basic': # Could add basic auth here if needed pass # Send webhook response = requests.post( url, json=payload, headers=headers, timeout=webhook_config.get('timeout_seconds', 10) ) if response.status_code in [200, 201, 202]: logger.info("Webhook sent successfully: {}".format(url)) return True else: logger.warning("Webhook failed: HTTP {} - {}".format( response.status_code, response.text[:200] )) return False except Exception as e: logger.error("Webhook error: {}".format(str(e))) return False