# 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: '

This is a test email sent via MailHog

' }); ``` #### 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: '

Test Email

This email was sent from Express using MailHog.

' }); 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 = '

HTML Email

This is HTML content

'; 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('

HTML Email

'); }); }); 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='

Test Email

This email was sent from Flask using MailHog.

' ) 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='

Test Email

Sent from Django using MailHog

', 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 Email Test

This is a formatted email.

''' 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('

Test Email

This is a test email from Laravel using MailHog.

', 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: '

Test Email

This email was sent from Rails using MailHog.

'.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 ```