This commit is contained in:
parent
f537273fd8
commit
19711f2153
10 changed files with 2602 additions and 2 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,3 +5,7 @@ tests/__pycache__
|
||||||
infra/.terraform
|
infra/.terraform
|
||||||
infra/.terraform.lock.hcl
|
infra/.terraform.lock.hcl
|
||||||
venv
|
venv
|
||||||
|
infra/email/.terraform
|
||||||
|
infra/email/.terraform.lock.hcl
|
||||||
|
infra/email/node_modules
|
||||||
|
infra/email/email_processor.zip
|
||||||
|
|
180
infra/email/email_processor.js
Normal file
180
infra/email/email_processor.js
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
||||||
|
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
|
const { simpleParser } = require('mailparser');
|
||||||
|
|
||||||
|
// Initialize clients
|
||||||
|
const sesClient = new SESClient({ region: process.env.AWS_REGION || 'us-west-2' });
|
||||||
|
const s3Client = new S3Client({ region: process.env.AWS_REGION || 'us-west-2' });
|
||||||
|
|
||||||
|
exports.handler = async (event) => {
|
||||||
|
try {
|
||||||
|
// Extract S3 event information
|
||||||
|
if (!event.Records || !event.Records[0] || !event.Records[0].s3) {
|
||||||
|
throw new Error('Invalid S3 event structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3Record = event.Records[0].s3;
|
||||||
|
const bucketName = s3Record.bucket.name;
|
||||||
|
const objectKey = s3Record.object.key;
|
||||||
|
|
||||||
|
// Filter out attachment files to prevent infinite loops
|
||||||
|
if (objectKey.startsWith('attachments/')) {
|
||||||
|
console.log(`Skipping attachment file: ${objectKey}`);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'Skipped attachment file',
|
||||||
|
reason: 'Object is in attachments/ prefix',
|
||||||
|
objectKey: objectKey
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing email: ${objectKey}`);
|
||||||
|
|
||||||
|
// Get the email content from S3
|
||||||
|
const getObjectParams = {
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: objectKey
|
||||||
|
};
|
||||||
|
|
||||||
|
const s3Response = await s3Client.send(new GetObjectCommand(getObjectParams));
|
||||||
|
const emailContent = await s3Response.Body.transformToString();
|
||||||
|
|
||||||
|
// Extract recipient from the object key path
|
||||||
|
const pathParts = objectKey.split('/');
|
||||||
|
const recipient = pathParts[0]; // e.g., "calculator"
|
||||||
|
|
||||||
|
const forwardEmail = process.env.FORWARD_EMAIL;
|
||||||
|
const domainName = process.env.DOMAIN_NAME || '127local.net';
|
||||||
|
const environment = process.env.ENVIRONMENT || 'production';
|
||||||
|
|
||||||
|
// Parse the email using mailparser
|
||||||
|
const parsedEmail = await simpleParser(emailContent);
|
||||||
|
|
||||||
|
// Extract email details
|
||||||
|
const subject = parsedEmail.subject || 'No Subject';
|
||||||
|
const from = parsedEmail.from?.text || 'unknown@example.com';
|
||||||
|
const emailBody = parsedEmail.text || parsedEmail.html || 'No body content';
|
||||||
|
|
||||||
|
// Create a clean, properly formatted forwarded email
|
||||||
|
const forwardSubject = `[${environment.toUpperCase()}] FWD: ${subject}`;
|
||||||
|
|
||||||
|
// Build the email body with proper formatting
|
||||||
|
let forwardBody = `Email forwarded from ${from} to ${recipient}@${domainName} (${environment.toUpperCase()})\n`;
|
||||||
|
forwardBody += `Forwarded at: ${new Date().toISOString()}\n`;
|
||||||
|
forwardBody += `\n--- Original Email ---\n\n`;
|
||||||
|
forwardBody += `Subject: ${subject}\n`;
|
||||||
|
forwardBody += `From: ${from}\n`;
|
||||||
|
forwardBody += `\n${emailBody}`;
|
||||||
|
|
||||||
|
// Process attachments and generate download links
|
||||||
|
let attachmentLinks = [];
|
||||||
|
if (parsedEmail.attachments && parsedEmail.attachments.length > 0) {
|
||||||
|
console.log(`Processing ${parsedEmail.attachments.length} attachments...`);
|
||||||
|
|
||||||
|
for (const attachment of parsedEmail.attachments) {
|
||||||
|
try {
|
||||||
|
// Create unique filename for S3
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const safeFilename = attachment.filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
||||||
|
const s3Key = `attachments/${recipient}/${timestamp}-${safeFilename}`;
|
||||||
|
|
||||||
|
// Upload attachment to S3
|
||||||
|
const uploadParams = {
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: s3Key,
|
||||||
|
Body: attachment.content,
|
||||||
|
ContentType: attachment.contentType,
|
||||||
|
Metadata: {
|
||||||
|
originalEmail: objectKey,
|
||||||
|
recipient: recipient,
|
||||||
|
sender: from,
|
||||||
|
originalFilename: attachment.filename
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await s3Client.send(new PutObjectCommand(uploadParams));
|
||||||
|
console.log(`Uploaded: ${attachment.filename}`);
|
||||||
|
|
||||||
|
// Generate pre-signed download URL (expires in 7 days)
|
||||||
|
const downloadUrl = await getSignedUrl(
|
||||||
|
s3Client,
|
||||||
|
new GetObjectCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: s3Key
|
||||||
|
}),
|
||||||
|
{ expiresIn: 604800 } // 7 days
|
||||||
|
);
|
||||||
|
|
||||||
|
attachmentLinks.push({
|
||||||
|
filename: attachment.filename,
|
||||||
|
type: attachment.contentType,
|
||||||
|
size: attachment.size,
|
||||||
|
downloadUrl: downloadUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to process attachment ${attachment.filename}:`, error);
|
||||||
|
// Continue with other attachments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attachment information with download links
|
||||||
|
if (attachmentLinks.length > 0) {
|
||||||
|
forwardBody += `\n\n--- Attachments (${attachmentLinks.length}) ---\n`;
|
||||||
|
attachmentLinks.forEach((attachment) => {
|
||||||
|
forwardBody += `• ${attachment.filename} (${attachment.type}, ${attachment.size} bytes)\n`;
|
||||||
|
forwardBody += ` Download: ${attachment.downloadUrl}\n`;
|
||||||
|
forwardBody += ` Note: Download link expires in 7 days\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the forwarded email using SendEmail
|
||||||
|
const forwardParams = {
|
||||||
|
Source: `noreply@${domainName}`,
|
||||||
|
Destination: {
|
||||||
|
ToAddresses: [forwardEmail]
|
||||||
|
},
|
||||||
|
Message: {
|
||||||
|
Subject: {
|
||||||
|
Data: forwardSubject
|
||||||
|
},
|
||||||
|
Body: {
|
||||||
|
Text: {
|
||||||
|
Data: forwardBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the properly formatted email
|
||||||
|
const result = await sesClient.send(new SendEmailCommand(forwardParams));
|
||||||
|
console.log(`Email forwarded successfully: ${result.MessageId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'Email forwarded successfully',
|
||||||
|
messageId: result.MessageId,
|
||||||
|
originalRecipient: recipient,
|
||||||
|
forwardedTo: forwardEmail,
|
||||||
|
s3Location: `${bucketName}/${objectKey}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing email:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: 'Failed to process email',
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
265
infra/email/main.tf
Normal file
265
infra/email/main.tf
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "calculator-127local-net-terraform-state"
|
||||||
|
key = "email/infrastructure/terraform.tfstate"
|
||||||
|
region = "us-west-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
region = var.aws_region
|
||||||
|
|
||||||
|
default_tags {
|
||||||
|
tags = local.common_tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common tags for all resources
|
||||||
|
locals {
|
||||||
|
common_tags = {
|
||||||
|
Project = "calculator-127local-net"
|
||||||
|
Environment = "production"
|
||||||
|
ManagedBy = "terraform"
|
||||||
|
Owner = "will@aerenserve.net"
|
||||||
|
Purpose = "email-forwarding"
|
||||||
|
CostCenter = "calculator-services"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# S3 bucket for email storage
|
||||||
|
resource "aws_s3_bucket" "email_storage" {
|
||||||
|
bucket = "calculator-127local-net-emails-${random_string.bucket_suffix.result}"
|
||||||
|
|
||||||
|
tags = merge(local.common_tags, {
|
||||||
|
Name = "Calculator Email Storage"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_string" "bucket_suffix" {
|
||||||
|
length = 8
|
||||||
|
special = false
|
||||||
|
upper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# S3 bucket versioning
|
||||||
|
resource "aws_s3_bucket_versioning" "email_storage" {
|
||||||
|
bucket = aws_s3_bucket.email_storage.id
|
||||||
|
versioning_configuration {
|
||||||
|
status = "Enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# S3 bucket server-side encryption
|
||||||
|
resource "aws_s3_bucket_server_side_encryption_configuration" "email_storage" {
|
||||||
|
bucket = aws_s3_bucket.email_storage.id
|
||||||
|
|
||||||
|
rule {
|
||||||
|
apply_server_side_encryption_by_default {
|
||||||
|
sse_algorithm = "AES256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# S3 bucket public access block
|
||||||
|
resource "aws_s3_bucket_public_access_block" "email_storage" {
|
||||||
|
bucket = aws_s3_bucket.email_storage.id
|
||||||
|
|
||||||
|
block_public_acls = true
|
||||||
|
block_public_policy = true
|
||||||
|
ignore_public_acls = true
|
||||||
|
restrict_public_buckets = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# SES Domain identity for 127local.net
|
||||||
|
resource "aws_ses_domain_identity" "calculator" {
|
||||||
|
domain = var.domain_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# SES Domain DKIM for 127local.net
|
||||||
|
resource "aws_ses_domain_dkim" "calculator" {
|
||||||
|
domain = aws_ses_domain_identity.calculator.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Route53 records for SES DKIM validation
|
||||||
|
resource "aws_route53_record" "ses_dkim" {
|
||||||
|
count = 3
|
||||||
|
zone_id = var.route53_zone_id
|
||||||
|
name = "${element(aws_ses_domain_dkim.calculator.dkim_tokens, count.index)}._domainkey.${var.domain_name}"
|
||||||
|
type = "CNAME"
|
||||||
|
ttl = "600"
|
||||||
|
records = ["${element(aws_ses_domain_dkim.calculator.dkim_tokens, count.index)}.dkim.amazonses.com"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# SES Email receiving rule set
|
||||||
|
resource "aws_ses_receipt_rule_set" "calculator" {
|
||||||
|
rule_set_name = "calculator-main-rule-set"
|
||||||
|
}
|
||||||
|
|
||||||
|
# SES Receipt rule for calculator@127local.net
|
||||||
|
resource "aws_ses_receipt_rule" "calculator_forward" {
|
||||||
|
name = "calculator-forward"
|
||||||
|
rule_set_name = aws_ses_receipt_rule_set.calculator.rule_set_name
|
||||||
|
recipients = ["calculator@${var.domain_name}"]
|
||||||
|
enabled = true
|
||||||
|
scan_enabled = true
|
||||||
|
tls_policy = "Optional"
|
||||||
|
|
||||||
|
add_header_action {
|
||||||
|
header_name = "X-Forwarded-To"
|
||||||
|
header_value = var.forward_email
|
||||||
|
position = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
s3_action {
|
||||||
|
bucket_name = aws_s3_bucket.email_storage.bucket
|
||||||
|
object_key_prefix = "calculator/"
|
||||||
|
position = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set this rule set as active
|
||||||
|
resource "aws_ses_active_receipt_rule_set" "calculator" {
|
||||||
|
rule_set_name = aws_ses_receipt_rule_set.calculator.rule_set_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda function for email processing
|
||||||
|
resource "aws_lambda_function" "email_processor" {
|
||||||
|
filename = "email_processor.zip"
|
||||||
|
function_name = "calculator-email-processor"
|
||||||
|
role = aws_iam_role.lambda_role.arn
|
||||||
|
handler = "email_processor.handler"
|
||||||
|
source_code_hash = data.archive_file.email_processor_zip.output_base64sha256
|
||||||
|
runtime = "nodejs18.x"
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = {
|
||||||
|
FORWARD_EMAIL = var.forward_email
|
||||||
|
DOMAIN_NAME = var.domain_name
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = merge(local.common_tags, {
|
||||||
|
Name = "Calculator Email Processor"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create zip file for Lambda function with dependencies
|
||||||
|
data "archive_file" "email_processor_zip" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "."
|
||||||
|
output_path = "email_processor.zip"
|
||||||
|
excludes = ["email_processor.zip", "*.tf", "*.tfvars", "README.md", ".terraform", ".terraform.lock.hcl"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# IAM role for Lambda function
|
||||||
|
resource "aws_iam_role" "lambda_role" {
|
||||||
|
name = "calculator-email-processor-role"
|
||||||
|
|
||||||
|
assume_role_policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Action = "sts:AssumeRole"
|
||||||
|
Effect = "Allow"
|
||||||
|
Principal = {
|
||||||
|
Service = "lambda.amazonaws.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
tags = local.common_tags
|
||||||
|
}
|
||||||
|
|
||||||
|
# IAM policy for Lambda function
|
||||||
|
resource "aws_iam_role_policy" "lambda_policy" {
|
||||||
|
name = "calculator-email-processor-policy"
|
||||||
|
role = aws_iam_role.lambda_role.id
|
||||||
|
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"logs:CreateLogGroup",
|
||||||
|
"logs:CreateLogStream",
|
||||||
|
"logs:PutLogEvents"
|
||||||
|
]
|
||||||
|
Resource = "arn:aws:logs:*:*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:PutObject"
|
||||||
|
]
|
||||||
|
Resource = "${aws_s3_bucket.email_storage.arn}/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"ses:SendEmail",
|
||||||
|
"ses:SendRawEmail"
|
||||||
|
]
|
||||||
|
Resource = "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# S3 bucket notification to trigger Lambda
|
||||||
|
resource "aws_s3_bucket_notification" "email_processor" {
|
||||||
|
bucket = aws_s3_bucket.email_storage.id
|
||||||
|
|
||||||
|
lambda_function {
|
||||||
|
lambda_function_arn = aws_lambda_function.email_processor.arn
|
||||||
|
events = ["s3:ObjectCreated:*"]
|
||||||
|
filter_prefix = "calculator/"
|
||||||
|
filter_suffix = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda permission for S3 to invoke the function
|
||||||
|
resource "aws_lambda_permission" "allow_s3" {
|
||||||
|
statement_id = "AllowExecutionFromS3Bucket"
|
||||||
|
action = "lambda:InvokeFunction"
|
||||||
|
function_name = aws_lambda_function.email_processor.function_name
|
||||||
|
principal = "s3.amazonaws.com"
|
||||||
|
source_arn = aws_s3_bucket.email_storage.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
# Outputs
|
||||||
|
output "email_storage_bucket_name" {
|
||||||
|
value = aws_s3_bucket.email_storage.bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ses_domain_identity" {
|
||||||
|
value = aws_ses_domain_identity.calculator.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ses_receipt_rule_set_name" {
|
||||||
|
value = aws_ses_receipt_rule_set.calculator.rule_set_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "lambda_function_name" {
|
||||||
|
value = aws_lambda_function.email_processor.function_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dkim_tokens" {
|
||||||
|
value = aws_ses_domain_dkim.calculator.dkim_tokens
|
||||||
|
}
|
2045
infra/email/package-lock.json
generated
Normal file
2045
infra/email/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
infra/email/package.json
Normal file
15
infra/email/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "calculator-email-processor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lambda function to process and forward emails for calculator.127local.net",
|
||||||
|
"main": "email_processor.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-ses": "^3.0.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.0.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.0.0",
|
||||||
|
"mailparser": "^3.6.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
58
infra/email/setup.sh
Normal file
58
infra/email/setup.sh
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Calculator Email Infrastructure Setup Script
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Setting up Calculator Email Infrastructure..."
|
||||||
|
|
||||||
|
# Check if we're in the right directory
|
||||||
|
if [ ! -f "main.tf" ]; then
|
||||||
|
echo "❌ Error: main.tf not found. Please run this script from infra/email directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install npm dependencies for Lambda function
|
||||||
|
echo "📦 Installing Lambda dependencies..."
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
npm install
|
||||||
|
else
|
||||||
|
echo "✅ Dependencies already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize Terraform if needed
|
||||||
|
if [ ! -d ".terraform" ]; then
|
||||||
|
echo "🔧 Initializing Terraform..."
|
||||||
|
terraform init
|
||||||
|
else
|
||||||
|
echo "✅ Terraform already initialized"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plan the infrastructure setup
|
||||||
|
echo "📋 Planning infrastructure setup..."
|
||||||
|
terraform plan
|
||||||
|
|
||||||
|
# Ask for confirmation
|
||||||
|
echo ""
|
||||||
|
read -p "🤔 Do you want to create this infrastructure? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "🚀 Creating infrastructure..."
|
||||||
|
terraform apply -auto-approve
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Infrastructure setup complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Next steps:"
|
||||||
|
echo "1. Go to AWS SES Console and verify the 127local.net domain identity"
|
||||||
|
echo "2. Wait for DKIM verification (should happen automatically)"
|
||||||
|
echo "3. Test by sending an email to calculator@127local.net"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Useful commands:"
|
||||||
|
echo " terraform output # Show all outputs"
|
||||||
|
echo " terraform output dkim_tokens # Show DKIM tokens"
|
||||||
|
echo " terraform output lambda_function_name # Show Lambda function name"
|
||||||
|
else
|
||||||
|
echo "❌ Infrastructure setup cancelled"
|
||||||
|
exit 1
|
||||||
|
fi
|
3
infra/email/terraform.tfvars
Normal file
3
infra/email/terraform.tfvars
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
aws_region = "us-west-2"
|
||||||
|
domain_name = "127local.net"
|
||||||
|
route53_zone_id = "Z001158010D1XENOLOOMC"
|
20
infra/email/variables.tf
Normal file
20
infra/email/variables.tf
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
variable "aws_region" {
|
||||||
|
description = "AWS region for resources"
|
||||||
|
type = string
|
||||||
|
default = "us-west-2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain_name" {
|
||||||
|
description = "Domain name for email forwarding"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "forward_email" {
|
||||||
|
description = "Email address to forward incoming emails to"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "route53_zone_id" {
|
||||||
|
description = "Route53 hosted zone ID for the domain"
|
||||||
|
type = string
|
||||||
|
}
|
|
@ -276,14 +276,21 @@ input,select,textarea{width:100%;background:transparent;color:var(--text);border
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-link,
|
||||||
.source-link {
|
.source-link {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-link:hover,
|
||||||
.source-link:hover {
|
.source-link:hover {
|
||||||
color: var(--accent2);
|
color: var(--accent2);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -33,7 +33,10 @@
|
||||||
<footer class="wrap foot">
|
<footer class="wrap foot">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<span>No tracking. No server. Everything runs in your browser.</span>
|
<span>No tracking. No server. Everything runs in your browser.</span>
|
||||||
<a href="https://code.disobey.net/whilb/calculator.127local.net" target="_blank" rel="noopener noreferrer" class="source-link">Source Code</a>
|
<div class="footer-links">
|
||||||
|
<a href="mailto:calculator@127local.net" class="contact-link">calculator@127local.net</a>
|
||||||
|
<a href="https://code.disobey.net/whilb/calculator.127local.net" target="_blank" rel="noopener noreferrer" class="source-link">Source Code</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="/js/app.js"></script>
|
<script type="module" src="/js/app.js"></script>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue