A2→A3 Email Now Includes: File Details: ✅ Original filename (from Box with Job# and Tracking ID) ✅ Clean filename (stripped for DAM) ✅ DAM Asset ID ✅ Tracking ID Processing Details: ✅ Master Asset ID (source asset) ✅ Upload folder ID (where it went in DAM) ✅ Box folder ID (where it came from) Complete Step-by-Step: ✅ Downloaded from Box (folder 348526703108) ✅ Loaded master metadata from database ✅ Built 27 MVP fields ✅ Updated Description from filename ✅ Updated Language from filename ✅ Set State to Local ✅ Stripped Job# and Tracking ID ✅ Uploaded to DAM ✅ Deleted from Box Now both A1→A2 and A2→A3 emails are extremely verbose! 🤖 Generated with Claude Code
266 lines
12 KiB
Python
266 lines
12 KiB
Python
"""
|
|
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):
|
|
"""
|
|
Send email via SMTP (Mailgun)
|
|
|
|
Args:
|
|
template_name: Name of email template
|
|
recipients: List of email addresses or single email
|
|
data: Template data dict
|
|
"""
|
|
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': """
|
|
<h2>Master Assets Downloaded Successfully</h2>
|
|
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
|
|
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
|
|
<p><strong>Assets Downloaded:</strong> {{ asset_count }}</p>
|
|
<p><strong>Status Updated:</strong> A1 → A2</p>
|
|
<hr>
|
|
|
|
<h3>Processed Assets:</h3>
|
|
<ul>
|
|
{% for asset in processed_assets %}
|
|
<li><strong>{{ asset.asset_name }}</strong>
|
|
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
|
|
<br>Box File ID: {{ asset.box_file_id }}
|
|
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<hr>
|
|
<p>All assets have been downloaded from DAM and uploaded to Box with tracking IDs.</p>
|
|
<p>Campaign status has been updated from A1 to A2.</p>
|
|
"""
|
|
},
|
|
'a2_to_a3_complete': {
|
|
'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}",
|
|
'html': """
|
|
<h2>Localized Assets Uploaded Successfully</h2>
|
|
<p><strong>Campaign:</strong> {{ campaign_name }}</p>
|
|
<p><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
|
<p><strong>Assets Uploaded:</strong> {{ asset_count }}</p>
|
|
<p><strong>Status Updated:</strong> A2 → A3</p>
|
|
<hr>
|
|
<p>All localized assets have been uploaded to DAM.</p>
|
|
"""
|
|
},
|
|
'upload_failed': {
|
|
'subject': "❌ Upload Failed - {filename}",
|
|
'html': """
|
|
<h2 style="color: red;">Upload Failed</h2>
|
|
<p><strong>Filename:</strong> {{ filename }}</p>
|
|
<p><strong>Tracking ID:</strong> {{ tracking_id }}</p>
|
|
<p><strong>Error:</strong> {{ error }}</p>
|
|
<hr>
|
|
<p>Please investigate the error.</p>
|
|
"""
|
|
},
|
|
'a1_to_a2_partial': {
|
|
'subject': "⚠️ Partial Download - Campaign {campaign_name}",
|
|
'html': """
|
|
<h2 style="color: orange;">Campaign Partially Processed</h2>
|
|
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
|
|
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
|
|
<p><strong>Total Assets:</strong> {{ total_assets }}</p>
|
|
<p><strong>Successful:</strong> {{ successful }}</p>
|
|
<p><strong>Failed:</strong> {{ failed }}</p>
|
|
<hr>
|
|
|
|
{% if successful > 0 %}
|
|
<h3 style="color: green;">✅ Successfully Processed ({{ successful }}):</h3>
|
|
<ul>
|
|
{% for asset in processed_assets %}
|
|
<li><strong>{{ asset.asset_name }}</strong>
|
|
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
|
|
<br>Box File ID: {{ asset.box_file_id }}
|
|
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
|
|
{% if failed > 0 %}
|
|
<h3 style="color: red;">❌ Failed Assets ({{ failed }}):</h3>
|
|
<ul>
|
|
{% for asset in failed_assets %}
|
|
<li><strong>{{ asset.asset_name }}</strong>
|
|
<br><span style="color: red;">Error: {{ asset.error }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
|
|
<hr>
|
|
<p style="color: red;"><strong>Status NOT updated.</strong> Campaign remains at A1.</p>
|
|
<p>The script will retry failed assets on the next run (every 5 minutes).</p>
|
|
"""
|
|
},
|
|
'a2_to_a3_file_uploaded': {
|
|
'subject': "✅ Asset Uploaded to DAM - {clean_filename}",
|
|
'html': """
|
|
<h2>Asset Uploaded Successfully (A2→A3)</h2>
|
|
|
|
<h3>File Details:</h3>
|
|
<p><strong>Original Filename (from Box):</strong><br>
|
|
<code>{{ filename }}</code></p>
|
|
<p><strong>Clean Filename (in DAM):</strong><br>
|
|
<code>{{ clean_filename }}</code></p>
|
|
<p><strong>DAM Asset ID:</strong> <code>{{ asset_id }}</code></p>
|
|
<p><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
|
|
|
|
<hr>
|
|
|
|
<h3>Processing Details:</h3>
|
|
<p><strong>Master Asset ID:</strong> {{ master_asset_name }}</p>
|
|
<p><strong>Uploaded to DAM Folder:</strong> {{ upload_folder }}</p>
|
|
<p><strong>Downloaded from Box Folder:</strong> {{ box_folder }}</p>
|
|
|
|
<hr>
|
|
|
|
<h3>What Was Done:</h3>
|
|
<ul>
|
|
<li>✅ Downloaded from Box processing folder (348526703108)</li>
|
|
<li>✅ Loaded master metadata from database ({{ tracking_id }})</li>
|
|
<li>✅ Built asset representation with 27 MVP fields</li>
|
|
<li>✅ Updated Description from filename</li>
|
|
<li>✅ Updated Language from filename</li>
|
|
<li>✅ Set State to "Local"</li>
|
|
<li>✅ Stripped OMG Job Number and Tracking ID from filename</li>
|
|
<li>✅ Uploaded to DAM Final Assets folder</li>
|
|
<li>✅ <strong>Deleted file from Box</strong></li>
|
|
</ul>
|
|
|
|
<hr>
|
|
<p><strong>Status:</strong> Asset processing complete.</p>
|
|
<p><em>Note: Campaign status will be updated to A3 once all assets are uploaded.</em></p>
|
|
"""
|
|
}
|
|
}
|
|
|
|
template_config = templates.get(template_name, {
|
|
'subject': 'Ferrero Automation Notification',
|
|
'html': '<p>{}</p>'.format(data)
|
|
})
|
|
|
|
# Render subject and body
|
|
subject = template_config['subject'].format(**data)
|
|
html_template = Template(template_config['html'])
|
|
html_body = html_template.render(**data)
|
|
|
|
# Prepare email
|
|
msg = MIMEMultipart('alternative')
|
|
msg['Subject'] = subject
|
|
msg['From'] = self.sender_email
|
|
msg['To'] = ', '.join(recipients) if isinstance(recipients, list) else recipients
|
|
|
|
# Attach HTML body
|
|
html_part = MIMEText(html_body, 'html')
|
|
msg.attach(html_part)
|
|
|
|
# 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(msg)
|
|
|
|
logger.info("Email sent via SMTP: {} to {}".format(template_name, recipients))
|
|
|
|
except Exception as e:
|
|
logger.error("Email error: {}".format(str(e)))
|
|
|
|
def send_webhook(self, url, payload):
|
|
"""
|
|
Send outgoing webhook notification
|
|
|
|
Args:
|
|
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
|