1108 lines
29 KiB
Markdown
1108 lines
29 KiB
Markdown
# MailHog Integration Patterns
|
|
|
|
## Framework Integration Guides
|
|
|
|
### Node.js Integration
|
|
|
|
#### Nodemailer Configuration
|
|
|
|
```javascript
|
|
const nodemailer = require('nodemailer');
|
|
|
|
// MailHog transporter configuration
|
|
const transporter = nodemailer.createTransporter({
|
|
host: 'localhost',
|
|
port: 1025,
|
|
secure: false, // TLS not required for MailHog
|
|
auth: false, // No authentication required
|
|
tls: {
|
|
rejectUnauthorized: false
|
|
}
|
|
});
|
|
|
|
// Send email function
|
|
async function sendEmail(options) {
|
|
try {
|
|
const info = await transporter.sendMail({
|
|
from: options.from || 'test@sender.local',
|
|
to: options.to,
|
|
subject: options.subject,
|
|
text: options.text,
|
|
html: options.html
|
|
});
|
|
|
|
console.log('Email sent successfully:', info.messageId);
|
|
return info;
|
|
} catch (error) {
|
|
console.error('Email sending failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Usage examples
|
|
await sendEmail({
|
|
to: 'recipient@example.com',
|
|
subject: 'Test Email',
|
|
text: 'This is a test email sent via MailHog',
|
|
html: '<p>This is a <strong>test email</strong> sent via MailHog</p>'
|
|
});
|
|
```
|
|
|
|
#### Express.js Integration
|
|
|
|
```javascript
|
|
const express = require('express');
|
|
const nodemailer = require('nodemailer');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
// Configure transporter for different environments
|
|
const getTransporter = () => {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
// Production SMTP configuration
|
|
return nodemailer.createTransporter({
|
|
host: process.env.SMTP_HOST,
|
|
port: process.env.SMTP_PORT,
|
|
secure: true,
|
|
auth: {
|
|
user: process.env.SMTP_USER,
|
|
pass: process.env.SMTP_PASS
|
|
}
|
|
});
|
|
} else {
|
|
// Development with MailHog
|
|
return nodemailer.createTransporter({
|
|
host: 'localhost',
|
|
port: 1025,
|
|
auth: false
|
|
});
|
|
}
|
|
};
|
|
|
|
// Email endpoint
|
|
app.post('/send-email', async (req, res) => {
|
|
try {
|
|
const transporter = getTransporter();
|
|
const { to, subject, text, html } = req.body;
|
|
|
|
await transporter.sendMail({
|
|
from: process.env.EMAIL_FROM || 'noreply@localhost',
|
|
to,
|
|
subject,
|
|
text,
|
|
html
|
|
});
|
|
|
|
res.json({ success: true, message: 'Email sent successfully' });
|
|
} catch (error) {
|
|
console.error('Email error:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// Test endpoint for development
|
|
app.get('/test-email', async (req, res) => {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return res.status(404).json({ error: 'Not available in production' });
|
|
}
|
|
|
|
try {
|
|
const transporter = getTransporter();
|
|
await transporter.sendMail({
|
|
to: 'test@recipient.local',
|
|
subject: 'Test Email from Express',
|
|
text: 'This is a test email from the Express application',
|
|
html: '<h1>Test Email</h1><p>This email was sent from Express using MailHog.</p>'
|
|
});
|
|
|
|
res.json({ success: true, message: 'Test email sent to MailHog' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.listen(3000, () => {
|
|
console.log('Server running on port 3000');
|
|
console.log('Environment:', process.env.NODE_ENV || 'development');
|
|
});
|
|
```
|
|
|
|
#### Jest Testing Integration
|
|
|
|
```javascript
|
|
// emailService.test.js
|
|
const nodemailer = require('nodemailer');
|
|
const MailHogClient = require('./mailhog-client');
|
|
|
|
describe('Email Service Integration', () => {
|
|
const mailhog = new MailHogClient();
|
|
const transporter = nodemailer.createTransporter({
|
|
host: 'localhost',
|
|
port: 1025,
|
|
auth: false
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clear messages before each test
|
|
await mailhog.deleteMessages('all');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up after each test
|
|
await mailhog.deleteMessages('all');
|
|
});
|
|
|
|
describe('Basic Email Sending', () => {
|
|
test('should send email to MailHog', async () => {
|
|
const emailData = {
|
|
from: 'sender@test.local',
|
|
to: 'recipient@test.local',
|
|
subject: 'Test Subject',
|
|
text: 'Test body content'
|
|
};
|
|
|
|
// Send email
|
|
const result = await transporter.sendMail(emailData);
|
|
expect(result.messageId).toBeDefined();
|
|
|
|
// Wait a moment for MailHog to process
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Verify in MailHog
|
|
const messages = await mailhog.searchMessages('to:recipient@test.local');
|
|
expect(messages.total).toBe(1);
|
|
|
|
const message = messages.items[0];
|
|
expect(message.Subject).toBe('Test Subject');
|
|
expect(message.From).toBe('sender@test.local');
|
|
});
|
|
|
|
test('should send HTML email', async () => {
|
|
const htmlContent = '<h1>HTML Email</h1><p>This is <strong>HTML content</strong></p>';
|
|
|
|
await transporter.sendMail({
|
|
from: 'test@sender.local',
|
|
to: 'html@test.local',
|
|
subject: 'HTML Test',
|
|
html: htmlContent
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
const messages = await mailhog.getMessages();
|
|
expect(messages.total).toBe(1);
|
|
|
|
const message = messages.items[0];
|
|
expect(message.Content.Body).toContain('<h1>HTML Email</h1>');
|
|
});
|
|
});
|
|
|
|
describe('Bulk Email Operations', () => {
|
|
test('should handle multiple recipients', async () => {
|
|
const recipients = ['user1@test.local', 'user2@test.local', 'user3@test.local'];
|
|
|
|
await transporter.sendMail({
|
|
from: 'bulk@sender.local',
|
|
to: recipients.join(', '),
|
|
subject: 'Bulk Email Test',
|
|
text: 'This email goes to multiple recipients'
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
const messages = await mailhog.getMessages();
|
|
expect(messages.total).toBe(1);
|
|
|
|
const message = messages.items[0];
|
|
const recipientAddresses = message.To.map(to => to.Address);
|
|
recipients.forEach(recipient => {
|
|
expect(recipientAddresses).toContain(recipient);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Python Integration
|
|
|
|
#### Flask Email Integration
|
|
|
|
```python
|
|
from flask import Flask, request, jsonify
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import os
|
|
|
|
app = Flask(__name__)
|
|
|
|
class EmailService:
|
|
def __init__(self):
|
|
self.config = self._get_config()
|
|
|
|
def _get_config(self):
|
|
if os.getenv('FLASK_ENV') == 'production':
|
|
return {
|
|
'host': os.getenv('SMTP_HOST'),
|
|
'port': int(os.getenv('SMTP_PORT', 587)),
|
|
'use_tls': True,
|
|
'username': os.getenv('SMTP_USER'),
|
|
'password': os.getenv('SMTP_PASS')
|
|
}
|
|
else:
|
|
# Development with MailHog
|
|
return {
|
|
'host': 'localhost',
|
|
'port': 1025,
|
|
'use_tls': False,
|
|
'username': None,
|
|
'password': None
|
|
}
|
|
|
|
def send_email(self, to_email, subject, text_body=None, html_body=None):
|
|
msg = MIMEMultipart('alternative')
|
|
msg['Subject'] = subject
|
|
msg['From'] = os.getenv('EMAIL_FROM', 'noreply@localhost')
|
|
msg['To'] = to_email
|
|
|
|
if text_body:
|
|
msg.attach(MIMEText(text_body, 'plain'))
|
|
if html_body:
|
|
msg.attach(MIMEText(html_body, 'html'))
|
|
|
|
try:
|
|
with smtplib.SMTP(self.config['host'], self.config['port']) as server:
|
|
if self.config['use_tls']:
|
|
server.starttls()
|
|
if self.config['username']:
|
|
server.login(self.config['username'], self.config['password'])
|
|
|
|
server.send_message(msg)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Email sending failed: {e}")
|
|
return False
|
|
|
|
email_service = EmailService()
|
|
|
|
@app.route('/send-email', methods=['POST'])
|
|
def send_email():
|
|
data = request.get_json()
|
|
success = email_service.send_email(
|
|
to_email=data['to'],
|
|
subject=data['subject'],
|
|
text_body=data.get('text'),
|
|
html_body=data.get('html')
|
|
)
|
|
|
|
return jsonify({'success': success})
|
|
|
|
@app.route('/test-email', methods=['GET'])
|
|
def test_email():
|
|
if os.getenv('FLASK_ENV') == 'production':
|
|
return jsonify({'error': 'Not available in production'}), 404
|
|
|
|
success = email_service.send_email(
|
|
to_email='test@recipient.local',
|
|
subject='Test Email from Flask',
|
|
text_body='This is a test email',
|
|
html_body='<h1>Test Email</h1><p>This email was sent from Flask using MailHog.</p>'
|
|
)
|
|
|
|
return jsonify({'success': success})
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True)
|
|
```
|
|
|
|
#### Django Integration
|
|
|
|
```python
|
|
# settings.py
|
|
import os
|
|
|
|
if os.getenv('DJANGO_ENV') == 'production':
|
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
EMAIL_HOST = os.getenv('SMTP_HOST')
|
|
EMAIL_PORT = int(os.getenv('SMTP_PORT', 587))
|
|
EMAIL_USE_TLS = True
|
|
EMAIL_HOST_USER = os.getenv('SMTP_USER')
|
|
EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASS')
|
|
else:
|
|
# Development with MailHog
|
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
EMAIL_HOST = 'localhost'
|
|
EMAIL_PORT = 1025
|
|
EMAIL_USE_TLS = False
|
|
EMAIL_HOST_USER = None
|
|
EMAIL_HOST_PASSWORD = None
|
|
|
|
DEFAULT_FROM_EMAIL = os.getenv('EMAIL_FROM', 'noreply@localhost')
|
|
|
|
# views.py
|
|
from django.core.mail import send_mail, send_mass_mail
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.conf import settings
|
|
import requests
|
|
|
|
@require_http_methods(["POST"])
|
|
def send_email_view(request):
|
|
data = request.json()
|
|
|
|
try:
|
|
send_mail(
|
|
subject=data['subject'],
|
|
message=data.get('message', ''),
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[data['to']],
|
|
html_message=data.get('html_message'),
|
|
fail_silently=False
|
|
)
|
|
return JsonResponse({'success': True})
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)})
|
|
|
|
@require_http_methods(["GET"])
|
|
def test_email_view(request):
|
|
if os.getenv('DJANGO_ENV') == 'production':
|
|
return JsonResponse({'error': 'Not available in production'}, status=404)
|
|
|
|
try:
|
|
send_mail(
|
|
subject='Test Email from Django',
|
|
message='This is a test email from Django',
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=['test@recipient.local'],
|
|
html_message='<h1>Test Email</h1><p>Sent from Django using MailHog</p>',
|
|
fail_silently=False
|
|
)
|
|
return JsonResponse({'success': True})
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)})
|
|
|
|
def check_mailhog_messages(request):
|
|
"""Utility endpoint to check MailHog messages during testing"""
|
|
if os.getenv('DJANGO_ENV') == 'production':
|
|
return JsonResponse({'error': 'Not available in production'}, status=404)
|
|
|
|
try:
|
|
response = requests.get('http://localhost:8025/api/v1/messages')
|
|
messages = response.json()
|
|
return JsonResponse({
|
|
'total': messages.get('total', 0),
|
|
'messages': [
|
|
{
|
|
'id': msg['ID'],
|
|
'from': msg['From'],
|
|
'to': [to['Address'] for to in msg['To']],
|
|
'subject': msg['Subject']
|
|
}
|
|
for msg in messages.get('items', [])
|
|
]
|
|
})
|
|
except Exception as e:
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
```
|
|
|
|
#### Pytest Integration
|
|
|
|
```python
|
|
# test_email_integration.py
|
|
import pytest
|
|
import requests
|
|
import time
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
|
|
class MailHogClient:
|
|
def __init__(self, base_url='http://localhost:8025'):
|
|
self.base_url = base_url
|
|
self.api_url = f'{base_url}/api/v1'
|
|
|
|
def get_messages(self):
|
|
response = requests.get(f'{self.api_url}/messages')
|
|
return response.json()
|
|
|
|
def search_messages(self, query):
|
|
response = requests.post(
|
|
f'{self.api_url}/search',
|
|
json={'query': query}
|
|
)
|
|
return response.json()
|
|
|
|
def delete_all_messages(self):
|
|
response = requests.delete(f'{self.api_url}/messages')
|
|
return response.status_code == 200
|
|
|
|
@pytest.fixture
|
|
def mailhog():
|
|
return MailHogClient()
|
|
|
|
@pytest.fixture
|
|
def smtp_transporter():
|
|
def create_transporter():
|
|
return smtplib.SMTP('localhost', 1025)
|
|
return create_transporter
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_mailhog(mailhog):
|
|
"""Clean up MailHog messages before and after each test"""
|
|
mailhog.delete_all_messages()
|
|
yield
|
|
mailhog.delete_all_messages()
|
|
|
|
class TestEmailIntegration:
|
|
def test_send_simple_email(self, mailhog, smtp_transporter):
|
|
"""Test sending a simple text email"""
|
|
server = smtp_transporter()
|
|
|
|
msg = MIMEText('This is a test email')
|
|
msg['Subject'] = 'Test Email'
|
|
msg['From'] = 'sender@test.local'
|
|
msg['To'] = 'recipient@test.local'
|
|
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
# Wait for MailHog to process
|
|
time.sleep(0.5)
|
|
|
|
# Verify email was received
|
|
messages = mailhog.search_messages('to:recipient@test.local')
|
|
assert messages['total'] == 1
|
|
|
|
message = messages['items'][0]
|
|
assert message['Subject'] == 'Test Email'
|
|
assert 'sender@test.local' in message['From']
|
|
|
|
def test_send_html_email(self, mailhog, smtp_transporter):
|
|
"""Test sending HTML email"""
|
|
server = smtp_transporter()
|
|
|
|
html_content = '''
|
|
<html>
|
|
<body>
|
|
<h1>HTML Email Test</h1>
|
|
<p>This is a <strong>formatted</strong> email.</p>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
msg = MIMEText(html_content, 'html')
|
|
msg['Subject'] = 'HTML Email Test'
|
|
msg['From'] = 'html@sender.local'
|
|
msg['To'] = 'html@recipient.local'
|
|
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
time.sleep(0.5)
|
|
|
|
messages = mailhog.search_messages('subject:HTML Email Test')
|
|
assert messages['total'] == 1
|
|
|
|
def test_multiple_recipients(self, mailhog, smtp_transporter):
|
|
"""Test email with multiple recipients"""
|
|
server = smtp_transporter()
|
|
|
|
recipients = ['user1@test.local', 'user2@test.local', 'user3@test.local']
|
|
|
|
msg = MIMEText('This email goes to multiple recipients')
|
|
msg['Subject'] = 'Multiple Recipients Test'
|
|
msg['From'] = 'bulk@sender.local'
|
|
msg['To'] = ', '.join(recipients)
|
|
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
time.sleep(0.5)
|
|
|
|
messages = mailhog.get_messages()
|
|
assert messages['total'] == 1
|
|
|
|
message = messages['items'][0]
|
|
recipient_addresses = [to['Address'] for to in message['To']]
|
|
|
|
for recipient in recipients:
|
|
assert recipient in recipient_addresses
|
|
```
|
|
|
|
### PHP Integration
|
|
|
|
#### Laravel Configuration
|
|
|
|
```php
|
|
// config/mail.php
|
|
return [
|
|
'default' => env('MAIL_MAILER', 'smtp'),
|
|
|
|
'mailers' => [
|
|
'smtp' => [
|
|
'transport' => 'smtp',
|
|
'host' => env('MAIL_HOST', 'localhost'),
|
|
'port' => env('MAIL_PORT', 1025),
|
|
'encryption' => env('MAIL_ENCRYPTION', null),
|
|
'username' => env('MAIL_USERNAME'),
|
|
'password' => env('MAIL_PASSWORD'),
|
|
'timeout' => null,
|
|
'auth_mode' => null,
|
|
],
|
|
],
|
|
|
|
'from' => [
|
|
'address' => env('MAIL_FROM_ADDRESS', 'noreply@localhost'),
|
|
'name' => env('MAIL_FROM_NAME', 'Example App'),
|
|
],
|
|
];
|
|
```
|
|
|
|
```php
|
|
// .env.example
|
|
# Development environment (MailHog)
|
|
MAIL_MAILER=smtp
|
|
MAIL_HOST=localhost
|
|
MAIL_PORT=1025
|
|
MAIL_ENCRYPTION=null
|
|
MAIL_USERNAME=null
|
|
MAIL_PASSWORD=null
|
|
MAIL_FROM_ADDRESS=noreply@localhost
|
|
MAIL_FROM_NAME="${APP_NAME}"
|
|
|
|
# Production environment
|
|
# MAIL_MAILER=smtp
|
|
# MAIL_HOST=smtp.mailprovider.com
|
|
# MAIL_PORT=587
|
|
# MAIL_ENCRYPTION=tls
|
|
# MAIL_USERNAME=your-email@example.com
|
|
# MAIL_PASSWORD=your-password
|
|
# MAIL_FROM_ADDRESS=noreply@example.com
|
|
```
|
|
|
|
```php
|
|
// app/Http/Controllers/EmailController.php
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
class EmailController extends Controller
|
|
{
|
|
public function sendEmail(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'to' => 'required|email',
|
|
'subject' => 'required|string|max:255',
|
|
'message' => 'required|string',
|
|
]);
|
|
|
|
try {
|
|
Mail::raw($validated['message'], function ($message) use ($validated) {
|
|
$message->to($validated['to'])
|
|
->subject($validated['subject']);
|
|
});
|
|
|
|
return response()->json(['success' => true]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function sendHtmlEmail(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'to' => 'required|email',
|
|
'subject' => 'required|string|max:255',
|
|
'html' => 'required|string',
|
|
]);
|
|
|
|
try {
|
|
Mail::html($validated['html'], function ($message) use ($validated) {
|
|
$message->to($validated['to'])
|
|
->subject($validated['subject']);
|
|
});
|
|
|
|
return response()->json(['success' => true]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function testEmail()
|
|
{
|
|
if (app()->environment('production')) {
|
|
return response()->json(['error' => 'Not available in production'], 404);
|
|
}
|
|
|
|
try {
|
|
Mail::html('<h1>Test Email</h1><p>This is a test email from Laravel using MailHog.</p>', function ($message) {
|
|
$message->to('test@recipient.local')
|
|
->subject('Laravel Test Email');
|
|
});
|
|
|
|
return response()->json(['success' => true]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Ruby on Rails Integration
|
|
|
|
```ruby
|
|
# config/environments/development.rb
|
|
Rails.application.configure do
|
|
# ... other configurations ...
|
|
|
|
config.action_mailer.delivery_method = :smtp
|
|
config.action_mailer.smtp_settings = {
|
|
address: 'localhost',
|
|
port: 1025,
|
|
authentication: 'plain',
|
|
user_name: nil,
|
|
password: nil,
|
|
enable_starttls_auto: false
|
|
}
|
|
|
|
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
|
|
end
|
|
|
|
# config/environments/production.rb
|
|
Rails.application.configure do
|
|
# ... other configurations ...
|
|
|
|
config.action_mailer.delivery_method = :smtp
|
|
config.action_mailer.smtp_settings = {
|
|
address: ENV.fetch('SMTP_HOST'),
|
|
port: ENV.fetch('SMTP_PORT', 587).to_i,
|
|
authentication: 'plain',
|
|
user_name: ENV.fetch('SMTP_USER'),
|
|
password: ENV.fetch('SMTP_PASS'),
|
|
domain: ENV.fetch('SMTP_DOMAIN'),
|
|
enable_starttls_auto: true
|
|
}
|
|
|
|
config.action_mailer.default_url_options = { host: ENV.fetch('APP_HOST') }
|
|
end
|
|
```
|
|
|
|
```ruby
|
|
# app/mailers/application_mailer.rb
|
|
class ApplicationMailer < ActionMailer::Base
|
|
default from: 'from@example.com'
|
|
layout 'mailer'
|
|
end
|
|
|
|
# app/mailers/user_mailer.rb
|
|
class UserMailer < ApplicationMailer
|
|
def welcome_email(user)
|
|
@user = user
|
|
mail(to: @user.email, subject: 'Welcome to Our App!')
|
|
end
|
|
|
|
def test_email
|
|
return false if Rails.env.production?
|
|
|
|
mail(to: 'test@recipient.local', subject: 'Rails Test Email') do |format|
|
|
format.text { render plain: 'This is a test email from Rails' }
|
|
format.html { render html: '<h1>Test Email</h1><p>This email was sent from Rails using MailHog.</p>'.html_safe }
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Testing Framework Integration
|
|
|
|
#### Cypress E2E Testing
|
|
|
|
```javascript
|
|
// cypress/support/commands.js
|
|
Cypress.Commands.add('checkMailhogEmail', (options = {}) => {
|
|
const { to, subject, timeout = 10000 } = options;
|
|
|
|
const startTime = Date.now();
|
|
|
|
const checkEmail = () => {
|
|
return cy.request('http://localhost:8025/api/v1/messages')
|
|
.then((response) => {
|
|
const messages = response.body.items;
|
|
|
|
let foundMessages = messages;
|
|
|
|
if (to) {
|
|
foundMessages = foundMessages.filter(msg =>
|
|
msg.To.some(recipient => recipient.Address.includes(to))
|
|
);
|
|
}
|
|
|
|
if (subject) {
|
|
foundMessages = foundMessages.filter(msg =>
|
|
msg.Subject && msg.Subject.includes(subject)
|
|
);
|
|
}
|
|
|
|
if (foundMessages.length > 0) {
|
|
return foundMessages[0];
|
|
}
|
|
|
|
if (Date.now() - startTime > timeout) {
|
|
throw new Error(`Email not found within ${timeout}ms`);
|
|
}
|
|
|
|
// Wait and retry
|
|
cy.wait(500);
|
|
return checkEmail();
|
|
});
|
|
};
|
|
|
|
return checkEmail();
|
|
});
|
|
|
|
Cypress.Commands.add('clearMailhog', () => {
|
|
return cy.request({
|
|
method: 'DELETE',
|
|
url: 'http://localhost:8025/api/v1/messages'
|
|
});
|
|
});
|
|
```
|
|
|
|
```javascript
|
|
// cypress/integration/email_spec.js
|
|
describe('Email Functionality', () => {
|
|
beforeEach(() => {
|
|
cy.clearMailhog();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.clearMailhog();
|
|
});
|
|
|
|
it('should send welcome email', () => {
|
|
const testEmail = `test${Date.now()}@example.com`;
|
|
|
|
// Fill out registration form
|
|
cy.visit('/register');
|
|
cy.get('[data-cy=email]').type(testEmail);
|
|
cy.get('[data-cy=password]').type('password123');
|
|
cy.get('[data-cy=register-button]').click();
|
|
|
|
// Verify email was sent
|
|
cy.checkMailhogEmail({
|
|
to: testEmail,
|
|
subject: 'Welcome'
|
|
}).then((email) => {
|
|
expect(email.Subject).to.contain('Welcome');
|
|
expect(email.From).to.contain('noreply');
|
|
});
|
|
});
|
|
|
|
it('should send password reset email', () => {
|
|
const testEmail = `reset${Date.now()}@example.com`;
|
|
|
|
// Request password reset
|
|
cy.visit('/forgot-password');
|
|
cy.get('[data-cy=email]').type(testEmail);
|
|
cy.get('[data-cy=reset-button]').click();
|
|
|
|
// Verify password reset email
|
|
cy.checkMailhogEmail({
|
|
to: testEmail,
|
|
subject: 'Password Reset'
|
|
}).then((email) => {
|
|
expect(email.Content.Body).to.contain('reset link');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
#### Playwright Testing
|
|
|
|
```javascript
|
|
// tests/helpers/mailhog.js
|
|
class MailHogClient {
|
|
constructor(baseUrl = 'http://localhost:8025') {
|
|
this.baseUrl = baseUrl;
|
|
this.apiUrl = `${baseUrl}/api/v1`;
|
|
}
|
|
|
|
async getMessages() {
|
|
const response = await fetch(`${this.apiUrl}/messages`);
|
|
return response.json();
|
|
}
|
|
|
|
async searchMessages(query) {
|
|
const response = await fetch(`${this.apiUrl}/search`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ query }),
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
async clearMessages() {
|
|
const response = await fetch(`${this.apiUrl}/messages`, {
|
|
method: 'DELETE',
|
|
});
|
|
return response.ok;
|
|
}
|
|
|
|
async waitForEmail(options = {}) {
|
|
const { to, subject, timeout = 10000 } = options;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const messages = await this.getMessages();
|
|
let foundMessages = messages.items;
|
|
|
|
if (to) {
|
|
foundMessages = foundMessages.filter(msg =>
|
|
msg.To.some(recipient => recipient.Address.includes(to))
|
|
);
|
|
}
|
|
|
|
if (subject) {
|
|
foundMessages = foundMessages.filter(msg =>
|
|
msg.Subject && msg.Subject.includes(subject)
|
|
);
|
|
}
|
|
|
|
if (foundMessages.length > 0) {
|
|
return foundMessages[0];
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
throw new Error(`Email not found within ${timeout}ms`);
|
|
}
|
|
}
|
|
|
|
module.exports = { MailHogClient };
|
|
```
|
|
|
|
```javascript
|
|
// tests/email.spec.js
|
|
const { test, expect } = require('@playwright/test');
|
|
const { MailHogClient } = require('./helpers/mailhog');
|
|
|
|
const mailhog = new MailHogClient();
|
|
|
|
test.beforeEach(async () => {
|
|
await mailhog.clearMessages();
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await mailhog.clearMessages();
|
|
});
|
|
|
|
test('should send welcome email', async ({ page }) => {
|
|
const testEmail = `test${Date.now()}@example.com`;
|
|
|
|
// Register user
|
|
await page.goto('/register');
|
|
await page.fill('[data-cy=email]', testEmail);
|
|
await page.fill('[data-cy=password]', 'password123');
|
|
await page.click('[data-cy=register-button]');
|
|
|
|
// Wait for welcome email
|
|
const email = await mailhog.waitForEmail({
|
|
to: testEmail,
|
|
subject: 'Welcome'
|
|
});
|
|
|
|
expect(email.Subject).toContain('Welcome');
|
|
expect(email.From).toContain('noreply');
|
|
});
|
|
|
|
test('should send notification email', async ({ page }) => {
|
|
// Login and trigger notification
|
|
await page.goto('/login');
|
|
await page.fill('[data-cy=email]', 'existing@example.com');
|
|
await page.fill('[data-cy=password]', 'password123');
|
|
await page.click('[data-cy=login-button]');
|
|
|
|
await page.click('[data-cy=trigger-notification]');
|
|
|
|
// Check for notification email
|
|
const email = await mailhog.waitForEmail({
|
|
to: 'existing@example.com',
|
|
subject: 'Notification'
|
|
});
|
|
|
|
expect(email.Content.Body).toContain('notification');
|
|
});
|
|
```
|
|
|
|
## CI/CD Integration Patterns
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
# .github/workflows/email-tests.yml
|
|
name: Email Integration Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
test-emails:
|
|
runs-on: ubuntu-latest
|
|
|
|
services:
|
|
mailhog:
|
|
image: mailhog/mailhog
|
|
ports:
|
|
- 1025:1025
|
|
- 8025:8025
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Wait for MailHog
|
|
run: |
|
|
timeout 60 bash -c 'until curl -f http://localhost:8025/api/v1/messages; do sleep 2; done'
|
|
|
|
- name: Run email tests
|
|
run: npm run test:email
|
|
env:
|
|
NODE_ENV: test
|
|
SMTP_HOST: localhost
|
|
SMTP_PORT: 1025
|
|
```
|
|
|
|
### Docker Compose Development
|
|
|
|
```yaml
|
|
# docker-compose.dev.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- NODE_ENV=development
|
|
- SMTP_HOST=mailhog
|
|
- SMTP_PORT=1025
|
|
depends_on:
|
|
- mailhog
|
|
volumes:
|
|
- .:/app
|
|
- /app/node_modules
|
|
command: npm run dev
|
|
|
|
mailhog:
|
|
image: mailhog/mailhog:latest
|
|
ports:
|
|
- "1025:1025" # SMTP
|
|
- "8025:8025" # UI/API
|
|
environment:
|
|
- MH_HOSTNAME=mailhog.docker.local
|
|
restart: unless-stopped
|
|
|
|
# Optional: MongoDB for persistent storage
|
|
mongodb:
|
|
image: mongo:latest
|
|
ports:
|
|
- "27017:27017"
|
|
volumes:
|
|
- mongodb_data:/data/db
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
mongodb_data:
|
|
```
|
|
|
|
### Kubernetes Development Environment
|
|
|
|
```yaml
|
|
# k8s/mailhog-dev.yaml
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: mailhog-dev
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: mailhog
|
|
namespace: mailhog-dev
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app: mailhog
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: mailhog
|
|
spec:
|
|
containers:
|
|
- name: mailhog
|
|
image: mailhog/mailhog:latest
|
|
ports:
|
|
- containerPort: 1025
|
|
- containerPort: 8025
|
|
env:
|
|
- name: MH_HOSTNAME
|
|
value: "mailhog.k8s.dev"
|
|
- name: MH_SMTP_BIND_ADDR
|
|
value: "0.0.0.0:1025"
|
|
- name: MH_UI_BIND_ADDR
|
|
value: "0.0.0.0:8025"
|
|
resources:
|
|
requests:
|
|
memory: "64Mi"
|
|
cpu: "50m"
|
|
limits:
|
|
memory: "128Mi"
|
|
cpu: "100m"
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: mailhog-smtp
|
|
namespace: mailhog-dev
|
|
spec:
|
|
selector:
|
|
app: mailhog
|
|
ports:
|
|
- port: 1025
|
|
targetPort: 1025
|
|
type: ClusterIP
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: mailhog-ui
|
|
namespace: mailhog-dev
|
|
spec:
|
|
selector:
|
|
app: mailhog
|
|
ports:
|
|
- port: 8025
|
|
targetPort: 8025
|
|
type: LoadBalancer
|
|
``` |