How to Automate Email Notifications in Scripts
Automated email notifications are essential components of modern system administration, monitoring, and application development. Whether you need to alert administrators about system failures, notify users about completed processes, or send regular status reports, automating email notifications can save time and ensure critical information reaches the right people promptly.
This comprehensive guide will walk you through implementing automated email notifications across different scripting environments, from basic setups to advanced configurations with error handling, templating, and security considerations.
Table of Contents
1. [Prerequisites and Requirements](#prerequisites-and-requirements)
2. [Understanding Email Automation Fundamentals](#understanding-email-automation-fundamentals)
3. [Python Email Automation](#python-email-automation)
4. [Bash Script Email Notifications](#bash-script-email-notifications)
5. [PowerShell Email Automation](#powershell-email-automation)
6. [Advanced Email Templates and Formatting](#advanced-email-templates-and-formatting)
7. [Error Handling and Retry Logic](#error-handling-and-retry-logic)
8. [Security Best Practices](#security-best-practices)
9. [Common Use Cases and Examples](#common-use-cases-and-examples)
10. [Troubleshooting Common Issues](#troubleshooting-common-issues)
11. [Best Practices and Professional Tips](#best-practices-and-professional-tips)
12. [Conclusion](#conclusion)
Prerequisites and Requirements
Before implementing automated email notifications, ensure you have the following prerequisites in place:
Technical Requirements
-
SMTP Server Access: Either a local mail server, cloud-based service (Gmail, Outlook, SendGrid), or corporate mail server
-
Network Connectivity: Outbound access to SMTP ports (typically 25, 587, or 465)
-
Authentication Credentials: Username, password, or API keys for your email service
-
Programming Environment: Python 3.x, Bash shell, or PowerShell depending on your chosen approach
Knowledge Prerequisites
- Basic understanding of email protocols (SMTP, TLS/SSL)
- Familiarity with your chosen scripting language
- Understanding of network security concepts
- Basic knowledge of email headers and MIME types
Email Service Configuration
Most modern email services require specific security settings:
-
Gmail: Enable "Less secure app access" or use App Passwords with 2FA
-
Outlook/Hotmail: Configure SMTP authentication
-
Corporate Exchange: Obtain SMTP relay permissions
-
Third-party Services: Configure API keys or SMTP credentials
Understanding Email Automation Fundamentals
SMTP Protocol Basics
Simple Mail Transfer Protocol (SMTP) is the standard protocol for sending emails. Understanding its basic components helps in troubleshooting and optimization:
-
SMTP Server: The mail server that processes and routes your emails
-
Port Numbers:
- Port 25: Standard SMTP (often blocked by ISPs)
- Port 587: SMTP with STARTTLS (recommended)
- Port 465: SMTP over SSL (legacy but still used)
-
Authentication: Username/password or token-based authentication
-
Encryption: TLS/SSL for secure transmission
Email Message Structure
Every email consists of headers and body content:
```
From: sender@example.com
To: recipient@example.com
Subject: Automated Notification
Date: Mon, 01 Jan 2024 12:00:00 +0000
Content-Type: text/html; charset=utf-8
System Alert
This is an automated notification.
```
Python Email Automation
Python provides robust libraries for email automation, making it an excellent choice for complex notification systems.
Basic Python Email Setup
First, let's create a simple email notification function:
```python
import smtplib
import ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os
from datetime import datetime
class EmailNotifier:
def __init__(self, smtp_server, smtp_port, username, password, use_tls=True):
self.smtp_server = smtp_server
self.smtp_port = smtp_port
self.username = username
self.password = password
self.use_tls = use_tls
def send_notification(self, to_email, subject, message, html_message=None, attachments=None):
"""
Send an email notification with optional HTML content and attachments
"""
try:
# Create message container
msg = MIMEMultipart('alternative')
msg['From'] = self.username
msg['To'] = to_email if isinstance(to_email, str) else ', '.join(to_email)
msg['Subject'] = subject
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
# Add plain text part
text_part = MIMEText(message, 'plain')
msg.attach(text_part)
# Add HTML part if provided
if html_message:
html_part = MIMEText(html_message, 'html')
msg.attach(html_part)
# Add attachments if provided
if attachments:
for file_path in attachments:
if os.path.isfile(file_path):
self._add_attachment(msg, file_path)
# Send the email
context = ssl.create_default_context()
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
if self.use_tls:
server.starttls(context=context)
server.login(self.username, self.password)
recipients = to_email if isinstance(to_email, list) else [to_email]
server.sendmail(self.username, recipients, msg.as_string())
print(f"Email sent successfully to {to_email}")
return True
except Exception as e:
print(f"Failed to send email: {str(e)}")
return False
def _add_attachment(self, msg, file_path):
"""Add file attachment to email message"""
with open(file_path, 'rb') as attachment:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename= {os.path.basename(file_path)}'
)
msg.attach(part)
```
Advanced Python Configuration
For production environments, create a configuration-driven approach:
```python
import json
import logging
from typing import List, Dict, Optional
class AdvancedEmailNotifier:
def __init__(self, config_file: str = None, config_dict: Dict = None):
"""
Initialize email notifier with configuration from file or dictionary
"""
if config_file:
with open(config_file, 'r') as f:
self.config = json.load(f)
elif config_dict:
self.config = config_dict
else:
raise ValueError("Either config_file or config_dict must be provided")
self.smtp_config = self.config['smtp']
self.default_sender = self.config.get('default_sender', self.smtp_config['username'])
# Setup logging
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
def send_alert(self, alert_type: str, message: str, severity: str = 'INFO',
custom_recipients: List[str] = None) -> bool:
"""
Send contextual alerts based on alert type and severity
"""
alert_config = self.config['alerts'].get(alert_type, {})
recipients = custom_recipients or alert_config.get('recipients', [])
if not recipients:
self.logger.warning(f"No recipients configured for alert type: {alert_type}")
return False
subject_template = alert_config.get('subject_template',
f"[{severity}] {alert_type} Alert")
subject = subject_template.format(
severity=severity,
timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
alert_type=alert_type
)
# Create HTML message with severity styling
html_message = self._create_alert_html(message, severity, alert_type)
return self._send_email(recipients, subject, message, html_message)
def _create_alert_html(self, message: str, severity: str, alert_type: str) -> str:
"""Create formatted HTML message based on severity"""
severity_colors = {
'CRITICAL': '#dc3545',
'ERROR': '#fd7e14',
'WARNING': '#ffc107',
'INFO': '#17a2b8',
'DEBUG': '#6c757d'
}
color = severity_colors.get(severity, '#17a2b8')
html_template = f"""
{message}
Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
return html_template
```
Bash Script Email Notifications
Bash scripts offer lightweight solutions for system-level notifications, particularly useful in Linux environments and cron jobs.
Using Built-in Mail Commands
Most Linux distributions include mail utilities:
```bash
#!/bin/bash
Configuration variables
SMTP_SERVER="smtp.gmail.com"
SMTP_PORT="587"
EMAIL_USER="your-email@gmail.com"
EMAIL_PASS="your-app-password"
FROM_EMAIL="alerts@yourserver.com"
Function to send simple text email
send_simple_email() {
local to_email="$1"
local subject="$2"
local message="$3"
echo "$message" | mail -s "$subject" "$to_email"
if [ $? -eq 0 ]; then
echo "Email sent successfully to $to_email"
return 0
else
echo "Failed to send email to $to_email"
return 1
fi
}
Function to send HTML email using sendmail
send_html_email() {
local to_email="$1"
local subject="$2"
local html_content="$3"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Create temporary file for email content
local temp_file=$(mktemp)
cat > "$temp_file" << EOF
To: $to_email
From: $FROM_EMAIL
Subject: $subject
Content-Type: text/html; charset=UTF-8
MIME-Version: 1.0
Date: $(date -R)
$html_content
EOF
# Send email using sendmail
if command -v sendmail >/dev/null 2>&1; then
sendmail "$to_email" < "$temp_file"
local result=$?
else
echo "sendmail not available, falling back to mail command"
mail -s "$subject" "$to_email" < <(echo "$html_content")
local result=$?
fi
# Clean up
rm -f "$temp_file"
if [ $result -eq 0 ]; then
echo "HTML email sent successfully to $to_email"
return 0
else
echo "Failed to send HTML email to $to_email"
return 1
fi
}
```
PowerShell Email Automation
PowerShell provides robust email capabilities, especially useful in Windows environments and Exchange integration.
Complete PowerShell Email Module
```powershell
PowerShell Email Notification Module
Save as EmailNotifications.psm1
function Send-EmailNotification {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string[]]$To,
[Parameter(Mandatory=$true)]
[string]$Subject,
[Parameter(Mandatory=$true)]
[string]$Body,
[string]$From = "notifications@yourcompany.com",
[string]$SmtpServer = "smtp.office365.com",
[int]$Port = 587,
[System.Management.Automation.PSCredential]$Credential,
[string[]]$Attachments = @(),
[switch]$BodyAsHtml,
[ValidateSet("Normal", "High", "Low")]
[string]$Priority = "Normal"
)
try {
$mailParams = @{
To = $To
From = $From
Subject = $Subject
Body = $Body
SmtpServer = $SmtpServer
Port = $Port
UseSsl = $true
BodyAsHtml = $BodyAsHtml.IsPresent
Priority = $Priority
}
if ($Credential) {
$mailParams.Credential = $Credential
}
if ($Attachments.Count -gt 0) {
$validAttachments = @()
foreach ($attachment in $Attachments) {
if (Test-Path $attachment) {
$validAttachments += $attachment
} else {
Write-Warning "Attachment not found: $attachment"
}
}
if ($validAttachments.Count -gt 0) {
$mailParams.Attachments = $validAttachments
}
}
Send-MailMessage @mailParams
Write-Host "Email sent successfully to: $($To -join ', ')" -ForegroundColor Green
return $true
}
catch {
Write-Error "Failed to send email: $($_.Exception.Message)"
return $false
}
}
function Get-EmailConfiguration {
$configPath = "$PSScriptRoot\EmailConfig.json"
if (Test-Path $configPath) {
return Get-Content $configPath | ConvertFrom-Json
} else {
# Return default configuration
return @{
SmtpServer = "smtp.office365.com"
Port = 587
UseSsl = $true
From = "alerts@yourcompany.com"
CriticalRecipients = @("admin@yourcompany.com")
ErrorRecipients = @("support@yourcompany.com")
WarningRecipients = @("monitoring@yourcompany.com")
InfoRecipients = @("logs@yourcompany.com")
}
}
}
function Get-EmailPriority {
param([string]$AlertLevel)
switch ($AlertLevel) {
"Critical" { return "High" }
"Error" { return "High" }
"Warning" { return "Normal" }
default { return "Normal" }
}
}
```
Advanced Email Templates and Formatting
Dynamic Template Engine
Create reusable email templates with dynamic content:
```python
from string import Template
import json
from datetime import datetime
class EmailTemplateEngine:
def __init__(self, template_dir="templates"):
self.template_dir = template_dir
self.templates = {}
def load_template(self, template_name):
"""Load email template from file"""
template_path = f"{self.template_dir}/{template_name}"
try:
with open(f"{template_path}.html", 'r') as f:
html_template = f.read()
with open(f"{template_path}.txt", 'r') as f:
text_template = f.read()
with open(f"{template_path}.json", 'r') as f:
template_config = json.load(f)
self.templates[template_name] = {
'html': Template(html_template),
'text': Template(text_template),
'config': template_config
}
except FileNotFoundError as e:
raise Exception(f"Template files not found: {e}")
def render_template(self, template_name, data):
"""Render template with provided data"""
if template_name not in self.templates:
self.load_template(template_name)
template = self.templates[template_name]
# Add common variables
common_data = {
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'date': datetime.now().strftime('%Y-%m-%d'),
'time': datetime.now().strftime('%H:%M:%S'),
'year': datetime.now().year
}
render_data = {
common_data, data}
html_content = template['html'].safe_substitute(render_data)
text_content = template['text'].safe_substitute(render_data)
return {
'html': html_content,
'text': text_content,
'subject': template['config'].get('subject_template', '').format(
render_data),
'priority': template['config'].get('priority', 'normal')
}
```
Sample HTML Template
Create `templates/system_alert.html`:
```html
System Alert
Alert Details
Message: $message
Description: $description
$memory_usage%
Memory Usage
System Information
- Hostname: $hostname
- IP Address: $ip_address
- Operating System: $os_info
- Uptime: $uptime
Recommended Actions
$recommended_actions
```
Error Handling and Retry Logic
Robust Error Handling Implementation
```python
import time
import logging
from functools import wraps
from typing import Callable, Any
def retry_on_failure(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
"""Decorator for retry logic with exponential backoff"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(
args, *kwargs) -> Any:
last_exception = None
current_delay = delay
for attempt in range(max_retries + 1):
try:
return func(
args, *kwargs)
except Exception as e:
last_exception = e
if attempt == max_retries:
logging.error(f"Failed after {max_retries + 1} attempts: {str(e)}")
break
logging.warning(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {current_delay} seconds...")
time.sleep(current_delay)
current_delay *= backoff
raise last_exception
return wrapper
return decorator
class RobustEmailNotifier:
def __init__(self, smtp_config, max_retries=3):
self.smtp_config = smtp_config
self.max_retries = max_retries
self.logger = logging.getLogger(__name__)
@retry_on_failure(max_retries=3, delay=2.0)
def send_email_with_retry(self, to_email, subject, message, html_message=None):
"""Send email with automatic retry on failure"""
try:
return self._send_email_internal(to_email, subject, message, html_message)
except smtplib.SMTPAuthenticationError:
self.logger.error("SMTP Authentication failed. Check credentials.")
raise
except smtplib.SMTPConnectError:
self.logger.error("Failed to connect to SMTP server. Check network connectivity.")
raise
except smtplib.SMTPRecipientsRefused:
self.logger.error("All recipients were refused. Check email addresses.")
raise
def _send_email_internal(self, to_email, subject, message, html_message=None):
"""Internal email sending method"""
# Implementation similar to previous examples
pass
def send_with_fallback(self, to_email, subject, message, fallback_methods=None):
"""Send email with fallback methods if primary fails"""
primary_success = False
try:
return self.send_email_with_retry(to_email, subject, message)
except Exception as e:
self.logger.error(f"Primary email method failed: {str(e)}")
if fallback_methods:
for method_name, method_func in fallback_methods.items():
try:
self.logger.info(f"Trying fallback method: {method_name}")
method_func(to_email, subject, message)
self.logger.info(f"Fallback method {method_name} succeeded")
return True
except Exception as fallback_error:
self.logger.error(f"Fallback method {method_name} failed: {str(fallback_error)}")
return False
```
Security Best Practices
Secure Credential Management
```python
import os
import keyring
from cryptography.fernet import Fernet
import base64
from typing import Dict, Optional
class SecureCredentialManager:
def __init__(self, encryption_key: bytes = None):
"""Initialize with optional encryption key for local storage"""
self.encryption_key = encryption_key or self._generate_key()
self.cipher = Fernet(self.encryption_key)
def _generate_key(self) -> bytes:
"""Generate a new encryption key"""
return Fernet.generate_key()
def store_credentials(self, service_name: str, username: str, password: str):
"""Store credentials securely using keyring"""
try:
keyring.set_password(service_name, username, password)
return True
except Exception as e:
print(f"Failed to store credentials: {str(e)}")
return False
def get_credentials(self, service_name: str, username: str) -> Optional[str]:
"""Retrieve credentials from keyring"""
try:
return keyring.get_password(service_name, username)
except Exception as e:
print(f"Failed to retrieve credentials: {str(e)}")
return None
def encrypt_config_file(self, config_data: Dict, file_path: str):
"""Encrypt and save configuration file"""
try:
json_data = json.dumps(config_data)
encrypted_data = self.cipher.encrypt(json_data.encode())
with open(file_path, 'wb') as f:
f.write(encrypted_data)
return True
except Exception as e:
print(f"Failed to encrypt config: {str(e)}")
return False
def decrypt_config_file(self, file_path: str) -> Optional[Dict]:
"""Decrypt and load configuration file"""
try:
with open(file_path, 'rb') as f:
encrypted_data = f.read()
decrypted_data = self.cipher.decrypt(encrypted_data)
return json.loads(decrypted_data.decode())
except Exception as e:
print(f"Failed to decrypt config: {str(e)}")
return None
Environment-based configuration
class EnvironmentConfig:
@staticmethod
def get_email_config() -> Dict:
"""Get email configuration from environment variables"""
return {
'smtp_server': os.getenv('SMTP_SERVER', 'localhost'),
'smtp_port': int(os.getenv('SMTP_PORT', '587')),
'username': os.getenv('SMTP_USERNAME'),
'password': os.getenv('SMTP_PASSWORD'),
'use_tls': os.getenv('SMTP_USE_TLS', 'true').lower() == 'true',
'timeout': int(os.getenv('SMTP_TIMEOUT', '30'))
}
@staticmethod
def validate_config(config: Dict) -> bool:
"""Validate that required configuration is present"""
required_fields = ['smtp_server', 'username', 'password']
for field in required_fields:
if not config.get(field):
raise ValueError(f"Missing required configuration: {field}")
return True
```
Common Use Cases and Examples
System Monitoring Alerts
```python
import psutil
import socket
class SystemMonitor:
def __init__(self, email_notifier, thresholds=None):
self.email_notifier = email_notifier
self.thresholds = thresholds or {
'cpu_percent': 80,
'memory_percent': 85,
'disk_percent': 90
}
def check_system_health(self):
"""Check system health and send alerts if thresholds exceeded"""
alerts = []
# CPU Usage
cpu_percent = psutil.cpu_percent(interval=1)
if cpu_percent > self.thresholds['cpu_percent']:
alerts.append({
'type': 'CPU_HIGH',
'level': 'WARNING',
'message': f'CPU usage is {cpu_percent}%',
'value': cpu_percent,
'threshold': self.thresholds['cpu_percent']
})
# Memory Usage
memory = psutil.virtual_memory()
if memory.percent > self.thresholds['memory_percent']:
alerts.append({
'type': 'MEMORY_HIGH',
'level': 'WARNING',
'message': f'Memory usage is {memory.percent}%',
'value': memory.percent,
'threshold': self.thresholds['memory_percent']
})
# Disk Usage
disk = psutil.disk_usage('/')
disk_percent = (disk.used / disk.total) * 100
if disk_percent > self.thresholds['disk_percent']:
alerts.append({
'type': 'DISK_HIGH',
'level': 'CRITICAL' if disk_percent > 95 else 'WARNING',
'message': f'Disk usage is {disk_percent:.1f}%',
'value': disk_percent,
'threshold': self.thresholds['disk_percent']
})
# Send alerts
for alert in alerts:
self.send_system_alert(alert)
return len(alerts) == 0
def send_system_alert(self, alert_data):
"""Send system alert email"""
hostname = socket.gethostname()
ip_address = socket.gethostbyname(hostname)
template_data = {
'alert_level': alert_data['level'],
'alert_type': alert_data['type'],
'message': alert_data['message'],
'hostname': hostname,
'ip_address': ip_address,
'threshold': alert_data['threshold'],
'current_value': alert_data['value']
}
self.email_notifier.send_template_email(
template_name='system_alert',
data=template_data,
recipients=['admin@company.com']
)
```
Backup Status Notifications
```bash
#!/bin/bash
Comprehensive backup notification script
BACKUP_LOG="/var/log/backup.log"
CONFIG_FILE="/etc/backup_notify.conf"
LOCK_FILE="/var/run/backup_notify.lock"
Source configuration
[ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"
Default values
BACKUP_PATH="${BACKUP_PATH:-/backup}"
RECIPIENTS="${RECIPIENTS:-admin@localhost}"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
MIN_BACKUP_SIZE="${MIN_BACKUP_SIZE:-100}" # MB
Functions
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$BACKUP_LOG"
}
check_backup_status() {
local backup_date=$(date '+%Y-%m-%d')
local backup_file="$BACKUP_PATH/backup-$backup_date.tar.gz"
local status="UNKNOWN"
local message=""
local details=""
if [ -f "$backup_file" ]; then
local file_size=$(stat -f%z "$backup_file" 2>/dev/null || stat -c%s "$backup_file" 2>/dev/null)
local size_mb=$((file_size / 1024 / 1024))
if [ $size_mb -ge $MIN_BACKUP_SIZE ]; then
status="SUCCESS"
message="Backup completed successfully"
details="File: $backup_file\nSize: ${size_mb}MB\nDate: $backup_date"
else
status="WARNING"
message="Backup file is smaller than expected"
details="File: $backup_file\nSize: ${size_mb}MB (expected: >= ${MIN_BACKUP_SIZE}MB)\nDate: $backup_date"
fi
else
status="ERROR"
message="Backup file not found for today"
details="Expected file: $backup_file\nDate: $backup_date"
fi
send_backup_notification "$status" "$message" "$details"
}
send_backup_notification() {
local status="$1"
local message="$2"
local details="$3"
local subject="[$status] Backup Report - $(hostname) - $(date '+%Y-%m-%d')"
local html_body=$(cat << EOF
Message: $message
Server: $(hostname)
Timestamp: $(date)
Recent Backup History:
$(ls -la $BACKUP_PATH/*.tar.gz 2>/dev/null | tail -5 || echo "No recent backups found")
EOF
)
send_html_email "$RECIPIENTS" "$subject" "$html_body"
log_message "Backup notification sent: $status - $message"
}
get_status_color() {
case "$1" in
"SUCCESS") echo "#28a745" ;;
"WARNING") echo "#ffc107" ;;
"ERROR") echo "#dc3545" ;;
*) echo "#6c757d" ;;
esac
}
Main execution
if [ -f "$LOCK_FILE" ]; then
echo "Another instance is running. Exiting."
exit 1
fi
echo $$ > "$LOCK_FILE"
trap "rm -f $LOCK_FILE" EXIT
log_message "Starting backup status check"
check_backup_status
log_message "Backup status check completed"
```
Troubleshooting Common Issues
Common SMTP Errors and Solutions
Authentication Failures
```python
def diagnose_smtp_auth(smtp_server, port, username, password):
"""Diagnose SMTP authentication issues"""
import smtplib
import ssl
try:
context = ssl.create_default_context()
server = smtplib.SMTP(smtp_server, port)
server.set_debuglevel(1) # Enable debug output
# Try to start TLS
try:
server.starttls(context=context)
print("✓ TLS connection established")
except Exception as e:
print(f"✗ TLS failed: {e}")
return False
# Try authentication
try:
server.login(username, password)
print("✓ Authentication successful")
except smtplib.SMTPAuthenticationError as e:
print(f"✗ Authentication failed: {e}")
print("Possible solutions:")
print("- Check username and password")
print("- Enable 'Less secure apps' for Gmail")
print("- Use App Password for 2FA accounts")
print("- Check if account is locked")
return False
server.quit()
return True
except Exception as e:
print(f"✗ Connection failed: {e}")
print("Possible solutions:")
print("- Check SMTP server address and port")
print("- Verify network connectivity")
print("- Check firewall settings")
return False
```
Network Connectivity Issues
```bash
#!/bin/bash
Network diagnostic script for email issues
diagnose_email_connectivity() {
local smtp_server="$1"
local smtp_port="$2"
echo "Diagnosing email connectivity to $smtp_server:$smtp_port"
echo "=================================================="
# Test DNS resolution
echo "1. Testing DNS resolution..."
if nslookup "$smtp_server" >/dev/null 2>&1; then
echo "✓ DNS resolution successful"
echo " IP: $(nslookup "$smtp_server" | grep -A1 "Name:" | tail -1 | awk '{print $2}')"
else
echo "✗ DNS resolution failed"
echo " Solution: Check DNS settings or use IP address"
return 1
fi
# Test network connectivity
echo "2. Testing network connectivity..."
if ping -c 3 "$smtp_server" >/dev/null 2>&1; then
echo "✓ Server is reachable"
else
echo "✗ Server is not reachable"
echo " Solution: Check network connection and firewall"
return 1
fi
# Test port connectivity
echo "3. Testing port connectivity..."
if timeout 10 bash -c "/dev/null; then
echo "✓ Port $smtp_port is open"
else
echo "✗ Port $smtp_port is not accessible"
echo " Solution: Check firewall rules and SMTP server configuration"
return 1
fi
# Test SMTP conversation
echo "4. Testing SMTP conversation..."
local smtp_response=$(timeout 10 telnet "$smtp_server" "$smtp_port" 2>/dev/null | head -1)
if [[ "$smtp_response" ==
"220" ]]; then
echo "✓ SMTP server responded correctly"
echo " Response: $smtp_response"
else
echo "✗ SMTP server did not respond correctly"
echo " Solution: Check SMTP server configuration"
return 1
fi
echo "All connectivity tests passed!"
return 0
}
Usage example
diagnose_email_connectivity "smtp.gmail.com" "587"
```
Email Formatting Issues
```python
def validate_email_content(subject, body, html_body=None):
"""Validate email content for common issues"""
issues = []
# Subject validation
if not subject or len(subject.strip()) == 0:
issues.append("Subject is empty")
elif len(subject) > 78:
issues.append("Subject is too long (>78 characters)")
# Body validation
if not body or len(body.strip()) == 0:
issues.append("Body is empty")
# HTML validation (basic)
if html_body:
if '' not in html_body.lower() and '' not in html_body.lower():
issues.append("HTML body may be malformed (missing html/body tags)")
# Check for unescaped characters
if '&' in html_body and '&' not in html_body:
issues.append("HTML body may contain unescaped ampersands")
# Character encoding issues
try:
subject.encode('utf-8')
body.encode('utf-8')
if html_body:
html_body.encode('utf-8')
except UnicodeEncodeError:
issues.append("Content contains characters that cannot be encoded as UTF-8")
return issues
```
Best Practices and Professional Tips
Performance Optimization
1.
Connection Pooling: Reuse SMTP connections for multiple emails
2.
Asynchronous Sending: Use threading or async libraries for bulk emails
3.
Rate Limiting: Respect SMTP server limits to avoid blacklisting
4.
Email Queuing: Implement queues for reliable delivery
```python
import asyncio
import aiosmtplib
from concurrent.futures import ThreadPoolExecutor
import queue
import threading
class OptimizedEmailSender:
def __init__(self, smtp_config, max_workers=5, rate_limit=10):
self.smtp_config = smtp_config
self.max_workers = max_workers
self.rate_limit = rate_limit # emails per second
self.email_queue = queue.Queue()
self.rate_limiter = threading.Semaphore(rate_limit)
async def send_bulk_emails_async(self, email_list):
"""Send multiple emails asynchronously"""
tasks = []
for email_data in email_list:
task = asyncio.create_task(self._send_single_email_async(email_data))
tasks.append(task)
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def _send_single_email_async(self, email_data):
"""Send single email asynchronously"""
try:
smtp = aiosmtplib.SMTP(
hostname=self.smtp_config['server'],
port=self.smtp_config['port'],
use_tls=True
)
await smtp.connect()
await smtp.login(
self.smtp_config['username'],
self.smtp_config['password']
)
await smtp.send_message(email_data['message'])
await smtp.quit()
return {"status": "success", "email": email_data['to']}
except Exception as e:
return {"status": "error", "email": email_data['to'], "error": str(e)}
```
Monitoring and Logging
```python
import logging
from datetime import datetime, timedelta
import json
class EmailMetrics:
def __init__(self, log_file="email_metrics.log"):
self.log_file = log_file
self.metrics = {
'sent': 0,
'failed': 0,
'retries': 0,
'last_sent': None,
'daily_stats': {}
}
# Setup logging
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
def record_email_sent(self, recipient, subject, status="success"):
"""Record email sending event"""
today = datetime.now().strftime('%Y-%m-%d')
if status == "success":
self.metrics['sent'] += 1
self.metrics['last_sent'] = datetime.now().isoformat()
else:
self.metrics['failed'] += 1
# Update daily stats
if today not in self.metrics['daily_stats']:
self.metrics['daily_stats'][today] = {'sent': 0, 'failed': 0}
self.metrics['daily_stats'][today][status] += 1
# Log the event
self.logger.info(f"Email {status}: to={recipient}, subject={subject}")
def get_daily_report(self, days=7):
"""Generate daily email report"""
report = {}
for i in range(days):
date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
report[date] = self.metrics['daily_stats'].get(date, {'sent': 0, 'failed': 0})
return report
def export_metrics(self, file_path):
"""Export metrics to JSON file"""
with open(file_path, 'w') as f:
json.dump(self.metrics, f, indent=2)
```
Email Template Best Practices
1.
Responsive Design: Ensure emails display well on all devices
2.
Alt Text: Include alt text for images
3.
Plain Text Fallback: Always provide plain text version
4.
Testing: Test emails across different clients
5.
Accessibility: Follow accessibility guidelines
```css
/
Email-safe CSS styles /
.email-container {
max-width: 600px;
margin: 0 auto;
font-family: Arial, Helvetica, sans-serif;
}
/
Media queries for mobile /
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
padding: 10px !important;
}
.responsive-table {
width: 100% !important;
}
.mobile-center {
text-align: center !important;
}
}
```
Security Checklist
- ✅ Use encrypted connections (TLS/SSL)
- ✅ Store credentials securely (environment variables, key vaults)
- ✅ Validate recipient email addresses
- ✅ Implement rate limiting
- ✅ Log email activities for audit
- ✅ Use authentication mechanisms (OAuth2, App Passwords)
- ✅ Sanitize email content to prevent injection
- ✅ Monitor for suspicious activity
Conclusion
Implementing automated email notifications in scripts requires careful consideration of multiple factors including security, reliability, performance, and maintainability. This comprehensive guide has covered the essential aspects of email automation across different platforms and programming languages.
Key Takeaways
1.
Choose the Right Tool: Select Python for complex scenarios, Bash for simple system alerts, and PowerShell for Windows environments.
2.
Security First: Always prioritize secure credential management and encrypted connections.
3.
Error Handling: Implement robust retry logic and fallback mechanisms to ensure reliable delivery.
4.
Template-Driven Approach: Use templates for consistent formatting and easy maintenance.
5.
Monitor and Log: Track email metrics and maintain logs for troubleshooting and compliance.
6.
Test Thoroughly: Validate your email system across different scenarios and failure conditions.
Next Steps
To further enhance your email notification system:
1.
Integration: Connect with monitoring tools like Nagios, Zabbix, or Prometheus
2.
Automation Platforms: Integrate with tools like Ansible, Jenkins, or GitLab CI
3.
Advanced Features: Implement email categorization, deduplication, and intelligent routing
4.
Scalability: Consider cloud-based email services for high-volume scenarios
5.
Analytics: Add email open tracking and engagement metrics
By following the practices and examples in this guide, you'll be able to create robust, secure, and maintainable email notification systems that enhance your operational capabilities and ensure critical information reaches the right people at the right time.
Remember to regularly review and update your email notification systems to adapt to changing requirements, security best practices, and technological advances. A well-implemented email notification system becomes an invaluable tool for maintaining system reliability and operational awareness.