Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
83e267a608 chore: merkwerk auto-update 2025-09-10 20:04:19 +02:00
f684ea1b4c bump version to 0.1.4 2025-09-10 20:04:19 +02:00
caeb5662d4 chore: merkwerk auto-update 2025-09-10 20:01:19 +02:00
f20915ff33 fix(smtp): add missing headers to prevent spam classification
Add required SMTP headers to fix spam classification issues:
- Message-ID: generated from timestamp and from_address domain
- MIME-Version: 1.0 header for proper email formatting
- Content-Transfer-Encoding: 8bit for UTF-8 content

Fixes rspamd spam score from 10.42/10.00 (reject) to 4.80/10.00 (clean)
by resolving MISSING_MID (-2.50), MISSING_MIME_VERSION (-2.00),
and R_BAD_CTE_7BIT (-1.05) penalties.

Tested with mail-tester.com (10/10 score) and production deployment
on tiamat shows successful delivery to inbox instead of spam folder.

Related DAW/infrastruktur#35
2025-09-10 20:00:34 +02:00
4af068e15c chore: merkwerk auto-update 2025-09-10 16:46:13 +02:00
7a921dc791 Release v0.1.3: Add STARTTLS support for port 587 2025-09-10 16:46:12 +02:00
ec7086259e chore: merkwerk auto-update 2025-09-10 16:45:13 +02:00
304b010a56 fix(smtp): add STARTTLS support for port 587
- Add STARTTLS handshake after EHLO for port 587
- Upgrade socket to SSL after STARTTLS command
- Perform second EHLO over encrypted connection
- Resolves authentication issues with Hetzner and other SMTP providers
- Fixes 'Must issue a STARTTLS command first' error

Closes #113
2025-09-10 16:45:12 +02:00
9cd8f4bce0 chore: merkwerk auto-update 2025-09-10 14:27:55 +02:00
f5d9f359de Release v0.1.2: Complete API Gateway with Multi-Tenant Mail 2025-09-10 14:27:54 +02:00
166325b133 chore: merkwerk auto-update 2025-09-10 12:20:50 +02:00
8b7806670c docs: simplify README and remove FreeBSD support
- Strip README to essentials with wiki references only
- Remove non-existent API docs and troubleshooting links
- Focus on quick start and actual integrations (merkwerk)
- Remove FreeBSD support from all installation scripts
- Clean up platform detection logic in scripts
- Maintain OpenBSD and Linux support only

Reduces maintenance burden and aligns with actual project scope.
2025-09-10 12:20:41 +02:00
6c60d88f62 Merge branch 'security/sanitize-test-scripts' 2025-09-07 21:26:52 +02:00
54c594e656 chore: merkwerk auto-update 2025-09-07 21:25:38 +02:00
08b49d3d75 security: sanitize internal infrastructure details from open source package
- Remove production_test_sequence.sh (DAW-specific production tests)
- Remove setup_env.sh (obsolete .env setup, replaced by furt.conf)
- Sanitize test scripts: replace dragons-at-work.de with example.com
- Sanitize API keys: replace dev keys with placeholder values
- Remove hardcoded DAW fallbacks from http_server.lua and smtp.lua
- Update .gitignore to exclude production-specific test files

Tests remain functional for developers with example domains.
All internal DAW infrastructure details removed from package.

Closes #101
2025-09-07 21:25:25 +02:00
baa2490bbe feat(security): systemd service hardening implementation
Merge feature/systemd-hardening

This merge introduces production-ready security hardening for the
systemd service with 6 pragmatic security options:

- ProtectSystem=strict for read-only filesystem
- ReadWritePaths for required directories only
- ProtectHome=yes to block home directory access
- NoNewPrivileges=yes to prevent privilege escalation
- PrivateTmp=yes for isolated temporary space
- RestrictAddressFamilies=AF_INET for IPv4-only networking

Testing completed successfully on:
- Debian 12 (systemd 247)
- Arch Linux (systemd 256)

No performance impact observed (812K RAM usage).

Closes DAW/furt#110
2025-09-07 19:11:37 +02:00
32c51e326e chore: merkwerk auto-update 2025-09-07 18:40:47 +02:00
24bd94dec4 feat(deployment): add systemd security hardening
- Add ProtectSystem=strict for read-only filesystem
- Add ReadWritePaths for required directories
- Add ProtectHome=yes to block home access
- Add NoNewPrivileges=yes to prevent escalation
- Add PrivateTmp=yes for isolated temp space
- Add RestrictAddressFamilies=AF_INET for IPv4-only

Related DAW/furt#110
2025-09-07 18:40:32 +02:00
77b9685231 Merge branch 'fix/validate-config-posix-regex' 2025-09-07 18:05:30 +02:00
b4bc104750 chore: merkwerk auto-update 2025-09-07 18:00:48 +02:00
683d6e5e5d fix(scripts): resolve POSIX regex compatibility in validate-config.sh
- Replace \s* with [ \t]* for POSIX-compatible whitespace matching
- Addresses false positive 'server port not configured' error
- Ensures validation works correctly across all POSIX-compliant systems

Related to DAW/furt#111
2025-09-07 18:00:41 +02:00
df1edf3dc5 feat(service): merge PID-file based service management (#100)
This merge introduces reliable cross-platform service detection using
PID-files instead of fragile pexp patterns, resolving rcctl check
issues on OpenBSD.

Key improvements:
- PID-file creation in /var/run/furt/ with proper permissions
- Updated start.sh for service vs interactive mode detection
- Fixed OpenBSD rc.d script with PID-file based rc_check()
- Corrected systemd service PIDFile parameter
- Enhanced setup-directories.sh for PID directory creation

Tested successfully on werner (OpenBSD):
- rcctl check furt now shows (ok) instead of (failed)
- Service start/stop/restart works reliably
- PID-file management handles permissions correctly

Closes #100
2025-09-07 17:41:33 +02:00
bbbbeef072 chore: merkwerk auto-update 2025-09-07 16:58:01 +02:00
59f372f2b0 feat(service): implement PID-file based service management
- Add PID directory creation in setup-directories.sh
- Update start.sh to use /var/run/furt/furt.pid for both platforms
- Fix OpenBSD rc.d script pidfile variable path
- Correct systemd service PIDFile parameter path
- Resolve rcctl check detection issues on OpenBSD

Fixes service detection problems where rcctl check would show (failed)
even when service was running. PID-file approach provides reliable
cross-platform service status detection instead of fragile pexp patterns.
Related DAW/furt#100
2025-09-07 16:57:35 +02:00
7ee990b052 chore: merkwerk auto-update 2025-09-05 22:30:13 +02:00
25a709ebbe feat(service): implement PID-file based service management (DAW/furt#100)
- Replace unreliable pexp patterns with PID-file approach
- Add graceful shutdown with timeout handling in rc.d script
- Implement process validation after startup
- Add SIGHUP config reload support for Unix services
- Ensure PID-file cleanup on service exit
- Update systemd service to use PIDFile parameter

Platform improvements:
- OpenBSD: rc_check/rc_stop functions now PID-file based
- Linux: systemd Type=forking with proper PIDFile support
- Cross-platform: /var/run/furt.pid standard location

Resolves service status detection issues where rcctl check showed
(failed) despite running service due to process name variations
across platforms.
2025-09-05 22:30:07 +02:00
25 changed files with 281 additions and 576 deletions

1
.gitignore vendored
View file

@ -68,3 +68,4 @@ config.production.lua
config/furt.conf
scripts/production_test_sequence.sh

View file

@ -20,3 +20,14 @@
795f8867,78e8ded,fix/json-library-compatibility,2025-09-05T15:44:42Z,michael,git,lua-api
795f8867,d4fa6e3,fix/ssl-dependency-check,2025-09-05T16:20:08Z,michael,git,lua-api
a670de0f,d271b84,refactor/extract-health-routes-and-server-core,2025-09-05T17:25:09Z,michael,git,lua-api
a670de0f,25a709e,feature/pid-file-service-management,2025-09-05T20:30:13Z,michael,git,lua-api
a670de0f,59f372f,feature/pid-file-service-management,2025-09-07T14:58:01Z,michael,git,lua-api
a670de0f,683d6e5,fix/validate-config-posix-regex,2025-09-07T16:00:48Z,michael,git,lua-api
a670de0f,24bd94d,feature/systemd-hardening,2025-09-07T16:40:47Z,michael,git,lua-api
4ee95dbc,08b49d3,security/sanitize-test-scripts,2025-09-07T19:25:38Z,michael,git,lua-api
59c85431,8b78066,main,2025-09-10T10:20:50Z,michael,git,lua-api
a71dd794,f5d9f35,main,2025-09-10T12:27:54Z,michael,git,lua-api
de5318f2,304b010,main,2025-09-10T14:45:12Z,michael,git,lua-api
980d67cd,7a921dc,main,2025-09-10T14:46:13Z,michael,git,lua-api
efbcbbd8,f20915f,main,2025-09-10T18:01:18Z,michael,git,lua-api
f777e765,f684ea1,main,2025-09-10T18:04:19Z,michael,git,lua-api

159
README.md
View file

@ -1,160 +1,83 @@
# Furt API Gateway
**HTTP-Server in Lua für Service-Integration**
**Pure Lua HTTP-Server für digitale Souveränität**
## Überblick
Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration.
Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys.
## Features
- HTTP-Server mit JSON-APIs
- Mail-Versendung über SMTP
- Request-Routing und Authentication
- Multi-Tenant Mail-Routing über SMTP
- API-Key-basierte Authentifizierung
- Health-Check-Endpoints
- Konfigurierbare Rate-Limiting
- Hugo/Website-Integration
- Rate-Limiting pro API-Key
- CORS-Support für Frontend-Integration
## Dependencies
## Quick Start
**Erforderlich:**
- `lua` 5.4+
- `lua-socket` (HTTP-Server)
- `lua-cjson` (JSON-Verarbeitung)
**Dependencies installieren:**
```bash
# OpenBSD
doas pkg_add lua lua-socket lua-cjson luasec
# Debian/Ubuntu
sudo apt install lua5.1 lua-socket lua-cjson lua-sec
# Arch Linux
sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec
```
**Installation:**
```bash
# Arch Linux
pacman -S lua lua-socket lua-cjson
# Ubuntu/Debian
apt install lua5.4 lua-socket lua-cjson
```
## Installation
```bash
# Repository klonen
git clone <repository-url>
git clone https://smida.dragons-at-work.de/DAW/furt.git
cd furt
# Scripts ausführbar machen
chmod +x scripts/*.sh
# Server starten
./scripts/start.sh
sudo ./install.sh
```
**Server läuft auf:** http://127.0.0.1:8080
**Server läuft auf:** http://127.0.0.1:7811
## API-Endpoints
### Health Check
**Health Check:**
```bash
GET /health
→ {"status":"healthy","service":"furt","version":"1.0.0"}
curl http://127.0.0.1:7811/health
```
### Mail senden
**Mail senden:**
```bash
POST /v1/mail/send
Content-Type: application/json
{
"name": "Name",
"email": "sender@example.com",
"message": "Nachricht"
}
→ {"success":true,"message":"Mail sent"}
```
## Konfiguration
**Environment Variables (.env):**
```bash
FURT_MAIL_HOST=mail.example.com
FURT_MAIL_PORT=587
FURT_MAIL_USERNAME=user@example.com
FURT_MAIL_PASSWORD=password
FURT_MAIL_TO=empfaenger@example.com
```
**Server-Config (config/server.lua):**
- Port und Host-Einstellungen
- API-Key-Konfiguration
- Rate-Limiting-Parameter
## Testing
**Automatische Tests:**
```bash
lua tests/test_http.lua
```
**Manuelle Tests:**
```bash
./scripts/test_curl.sh
# Oder direkt:
curl -X POST http://127.0.0.1:8080/v1/mail/send \
curl -X POST http://127.0.0.1:7811/v1/mail/send \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","message":"Test"}'
-d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}'
```
## Deployment
## Dokumentation
**OpenBSD:**
- rc.d-Script in `deployment/openbsd/`
- Systemd-Integration über Scripts
**Production-Setup:**
```bash
# Environment-Config kopieren
cp .env.example .env.production
# → SMTP-Credentials anpassen
# Production-Mode starten
export FURT_ENV=production
./scripts/start.sh
```
**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
## Projektstruktur
```
furt/
├── src/ # Lua-Source-Code
│ ├── main.lua # HTTP-Server
│ ├── routes/ # API-Endpoints
│ └── smtp.lua # Mail-Integration
├── config/ # Konfiguration
├── scripts/ # Start/Test-Scripts
├── tests/ # Test-Suite
└── deployment/ # System-Integration
├── config/ # Konfiguration
├── scripts/ # Installation & Management
└── deployment/ # System-Integration
```
## Hugo-Integration
## Integration
**Shortcode-Beispiel:**
```html
<form action="http://your-server:8080/v1/mail/send" method="POST">
<input name="name" type="text" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<button type="submit">Senden</button>
</form>
```
**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk)
## Development
## License
**Code-Struktur:**
- Module unter 200 Zeilen
- Funktionen unter 50 Zeilen
- Klare Fehlerbehandlung
- Testbare Komponenten
ISC - Siehe [LICENSE](LICENSE) für Details.
**Dependencies minimal halten:**
- Nur lua-socket und lua-cjson
- Keine externen HTTP-Libraries
- Standard-Lua-Funktionen bevorzugen
## Links
- **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt)
- **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
- **Projekt:** [Dragons@Work](https://dragons-at-work.de)

View file

@ -1 +1 @@
0.1.1
0.1.4

View file

@ -1,18 +1,33 @@
[Unit]
Description=furt Multi-Tenant API Gateway
Description=furt Multi-Tenant API Gateway (Security-Hardened)
After=network.target
[Service]
Type=forking
User=furt
Group=furt
ExecStart=/usr/local/share/furt/scripts/start.sh start
ExecStart=/usr/local/share/furt/scripts/start.sh
PIDFile=/var/run/furt/furt.pid
WorkingDirectory=/usr/local/share/furt
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# === SECURITY HARDENING ===
# Filesystem Protection
ProtectSystem=strict
ReadWritePaths=/var/run/furt /var/log/furt
ProtectHome=yes
# Process Hardening
NoNewPrivileges=yes
PrivateTmp=yes
# Network Restriction
RestrictAddressFamilies=AF_INET
[Install]
WantedBy=multi-user.target

View file

@ -3,11 +3,52 @@
daemon="/usr/local/share/furt/scripts/start.sh"
daemon_user="_furt"
daemon_cwd="/usr/local/share/furt"
daemon_flags="start"
. /etc/rc.d/rc.subr
pexp="lua.*src/main.lua"
# PID-File location
pidfile="/var/run/furt/furt.pid"
# Custom rc_check function (PID-File based)
rc_check() {
[ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null
}
# Custom rc_stop function (PID-File based)
rc_stop() {
if [ -f "$pidfile" ]; then
local _pid=$(cat "$pidfile")
echo "Stopping furt (PID: $_pid)"
kill "$_pid" 2>/dev/null
# Wait for process to die
local _timeout=10
while [ $_timeout -gt 0 ] && kill -0 "$_pid" 2>/dev/null; do
sleep 1
_timeout=$((_timeout - 1))
done
# Force kill if still running
if kill -0 "$_pid" 2>/dev/null; then
echo "Force killing furt (PID: $_pid)"
kill -9 "$_pid" 2>/dev/null
fi
rm -f "$pidfile"
echo "furt stopped"
else
echo "furt not running (no PID-File)"
fi
}
# Custom rc_reload function (signal-based)
rc_reload() {
if rc_check; then
local _pid=$(cat "$pidfile")
echo "Reloading furt configuration (PID: $_pid)"
kill -HUP "$_pid"
else
echo "furt not running"
return 1
fi
}
rc_cmd $1

View file

@ -1,176 +0,0 @@
# Multi-Tenant furt Setup-Anleitung
## Installation
### 1. Dateien platzieren
```bash
# OpenBSD/FreeBSD
mkdir -p /usr/local/etc/furt
mkdir -p /usr/local/share/furt
# Oder Linux
mkdir -p /etc/furt
mkdir -p /usr/local/share/furt
# Source code
cp -r src/ /usr/local/share/furt/
cp -r config/ /usr/local/share/furt/
```
### 2. Konfiguration erstellen
```bash
# Beispiel-Config kopieren und anpassen
# OpenBSD/FreeBSD:
cp furt.conf.example /usr/local/etc/furt/furt.conf
# Linux:
cp furt.conf.example /etc/furt/furt.conf
# Config editieren
vi /usr/local/etc/furt/furt.conf # oder /etc/furt/furt.conf
```
### 3. Start-Script
```bash
#!/bin/sh
# /usr/local/bin/furt
cd /usr/local/share/furt
lua src/main.lua
```
## Multi-Tenant Konfiguration
### Beispiel für 3 Websites
```ini
[server]
host = 127.0.0.1
port = 8080
[smtp_default]
host = mail.dragons-at-work.de
port = 465
user = noreply@dragons-at-work.de
password = your-smtp-password
# Website 1: Dragons@Work
[api_key "daw-key-abc123"]
name = "Dragons@Work Website"
permissions = mail:send
allowed_ips = 1.2.3.4/32, 10.0.0.0/8
mail_to = admin@dragons-at-work.de
mail_from = noreply@dragons-at-work.de
mail_subject_prefix = "[DAW] "
# Website 2: Biocodie (gleiche SMTP, andere Empfänger)
[api_key "bio-key-def456"]
name = "Biocodie Website"
permissions = mail:send
allowed_ips = 5.6.7.8/32
mail_to = contact@biocodie.de
mail_from = noreply@biocodie.de
mail_subject_prefix = "[Biocodie] "
# Website 3: Kunde mit eigenem SMTP
[api_key "kunde-key-ghi789"]
name = "Kunde X Website"
permissions = mail:send
allowed_ips = 9.10.11.12/32
mail_to = info@kunde-x.de
mail_from = noreply@kunde-x.de
mail_smtp_host = mail.kunde-x.de
mail_smtp_user = noreply@kunde-x.de
mail_smtp_pass = kunde-smtp-password
```
## Admin-Workflow
### Neue Website hinzufügen
1. **Config editieren:**
```bash
vi /usr/local/etc/furt/furt.conf
```
2. **Neuen API-Key-Block hinzufügen:**
```ini
[api_key "neue-website-key"]
name = "Neue Website"
permissions = mail:send
allowed_ips = 12.34.56.78/32
mail_to = contact@neue-website.de
mail_from = noreply@neue-website.de
```
3. **furt neu starten:**
```bash
systemctl restart furt
# oder
pkill -f "lua.*main.lua" && /usr/local/bin/furt &
```
### Website testen
```bash
# Test mit curl
curl -X POST http://localhost:8080/v1/mail/send \
-H "X-API-Key: neue-website-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"subject": "Test Message",
"message": "This is a test message"
}'
```
## Vorteile des Multi-Tenant-Systems
### ✅ Ein Server, viele Websites
- Alle Websites nutzen eine furt-Instanz
- Jede Website hat eigenen API-Key
- Verschiedene Empfänger-Adressen
- Verschiedene SMTP-Server möglich
### ✅ Admin-freundlich
- Nginx-style Config-Format
- Einfach neue Websites hinzufügen
- Klare Struktur pro Website
- Kommentare möglich
### ✅ Sicher
- IP-Restrictions pro Website
- Permissions pro API-Key
- Separate SMTP-Credentials möglich
- Rate-Limiting bleibt erhalten
### ✅ Flexibel
- Default SMTP + website-spezifische SMTP
- Subject-Prefix pro Website
- Verschiedene Mail-Adressen
- Beliebig viele Websites
## Backward Compatibility
Das neue System ist **vollständig kompatibel** mit der alten config/server.lua API. Bestehende Module (auth.lua, main.lua, etc.) funktionieren ohne Änderungen.
## Troubleshooting
### Config-Parsing-Fehler
```bash
# Config-Syntax prüfen
lua -e "require('src.config_parser').parse_file('/usr/local/etc/furt/furt.conf')"
```
### Mail-Routing testen
```bash
# Logs anschauen
tail -f /var/log/furt.log
# Debug-Mode
FURT_DEBUG=true lua src/main.lua
```

View file

@ -102,7 +102,7 @@ else
echo ""
echo "Next steps:"
echo "1. Edit configuration file:"
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
if [ "$(uname)" = "OpenBSD" ]; then
echo " /usr/local/etc/furt/furt.conf"
else
echo " /etc/furt/furt.conf"

0
scripts/cleanup_debug.sh Normal file → Executable file
View file

View file

@ -15,25 +15,25 @@ if [ "$(uname)" = "OpenBSD" ]; then
echo "Error: deployment/openbsd/rc.d-furt template not found"
exit 1
fi
cp deployment/openbsd/rc.d-furt /etc/rc.d/furt
chmod +x /etc/rc.d/furt
echo "furt_flags=" >> /etc/rc.conf.local
rcctl enable furt
echo "OpenBSD service created and enabled using repository template"
elif [ "$(uname)" = "Linux" ]; then
# Use systemd template from repository
if [ ! -f "deployment/linux/furt.service" ]; then
echo "Error: deployment/linux/furt.service template not found"
exit 1
fi
cp deployment/linux/furt.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable furt
echo "Linux systemd service created and enabled using repository template"
else
echo "Unsupported operating system for service creation"
exit 1

4
scripts/manual_mail_test.sh Normal file → Executable file
View file

@ -4,11 +4,11 @@
echo "Testing SMTP with corrected JSON..."
# Simple test without timestamp embedding
curl -X POST http://127.0.0.1:8080/v1/mail/send \
curl -X POST http://127.0.0.1:7811/v1/mail/send \
-H "Content-Type: application/json" \
-d '{
"name": "Furt Test User",
"email": "michael@dragons-at-work.de",
"email": "admin@example.com",
"subject": "Furt SMTP Test Success!",
"message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!"
}'

View file

@ -1,80 +0,0 @@
#!/bin/bash
# Production Test für api.dragons-at-work.de
echo "Testing Production API via Apache Proxy"
echo "======================================="
# Test 1: HTTPS Health Check
echo ""
echo "[1] Testing HTTPS Health Check..."
https_health=$(curl -s https://api.dragons-at-work.de/health)
echo "HTTPS Response: $https_health"
if echo "$https_health" | grep -q "healthy"; then
echo "[OK] HTTPS Proxy working"
else
echo "[ERROR] HTTPS Proxy failed"
exit 1
fi
# Test 2: SMTP Status via HTTPS
echo ""
echo "[2] Testing SMTP Configuration via HTTPS..."
if echo "$https_health" | grep -q '"smtp_configured":true'; then
echo "[OK] SMTP configured and accessible via HTTPS"
else
echo "[ERROR] SMTP not configured or not accessible"
fi
# Test 3: CORS Headers
echo ""
echo "[3] Testing CORS Headers..."
cors_test=$(curl -s -I https://api.dragons-at-work.de/health | grep -i "access-control")
if [ -n "$cors_test" ]; then
echo "[OK] CORS headers present: $cors_test"
else
echo "[WARN] CORS headers missing - add to Apache config"
fi
# Test 4: Production Mail Test
echo ""
echo "[4] Testing Production Mail via HTTPS..."
echo "WARNING: This sends real email via production API"
read -p "Continue with production mail test? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Sending production test email..."
prod_mail_response=$(curl -s -X POST https://api.dragons-at-work.de/v1/mail/send \
-H "Content-Type: application/json" \
-d '{
"name": "Production Test",
"email": "test@dragons-at-work.de",
"subject": "Production API Test - Apache Proxy Success!",
"message": "This email was sent via the production API at api.dragons-at-work.de through Apache proxy to Furt-Lua backend. HTTPS integration working!"
}')
echo "Production Response: $prod_mail_response"
if echo "$prod_mail_response" | grep -q '"success":true'; then
echo "[OK] PRODUCTION MAIL SENT VIA HTTPS!"
echo "Check admin@dragons-at-work.de for delivery confirmation"
else
echo "[ERROR] Production mail failed"
fi
else
echo "Skipping production mail test"
fi
# Test 5: Security Headers
echo ""
echo "[5] Testing Security Headers..."
security_headers=$(curl -s -I https://api.dragons-at-work.de/health)
echo "Security Headers:"
echo "$security_headers" | grep -i "x-content-type-options\|x-frame-options\|strict-transport"
echo ""
echo "Production Test Complete!"
echo "========================"
echo "Next: Hugo integration with https://api.dragons-at-work.de/v1/mail/send"

View file

@ -4,7 +4,7 @@
set -e
# Detect operating system for config directory
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
if [ "$(uname)" = "OpenBSD" ]; then
CONFIG_DIR="/usr/local/etc/furt"
USER="_furt"
GROUP="_furt"
@ -18,12 +18,15 @@ fi
mkdir -p "$CONFIG_DIR"
mkdir -p /usr/local/share/furt
mkdir -p /var/log/furt
mkdir -p /var/run/furt
# Set ownership for log directory (service user needs write access)
chown "$USER:$GROUP" /var/log/furt
chown "$USER:$GROUP" /var/run/furt
echo "Created directories:"
echo " Config: $CONFIG_DIR"
echo " Share: /usr/local/share/furt"
echo " Logs: /var/log/furt (owned by $USER)"
echo " PID: /var/run/furt (owned by $USER)"

View file

@ -4,7 +4,7 @@
set -e
# Detect operating system
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
if [ "$(uname)" = "OpenBSD" ]; then
# BSD systems use _furt user convention
groupadd _furt 2>/dev/null || true
useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true

View file

@ -1,101 +0,0 @@
#!/bin/bash
# furt-lua/scripts/setup_env.sh
# Add SMTP environment variables to existing .env (non-destructive)
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Furt SMTP Environment Setup ===${NC}"
# Navigate to furt project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
ENV_FILE="$PROJECT_ROOT/.env"
echo -e "${YELLOW}Project root:${NC} $PROJECT_ROOT"
echo -e "${YELLOW}Environment file:${NC} $ENV_FILE"
# Check if .env exists
if [ ! -f "$ENV_FILE" ]; then
echo -e "${YELLOW}Creating new .env file...${NC}"
cat > "$ENV_FILE" << 'EOF'
# Dragons@Work Project Environment Variables
# Furt SMTP Configuration for mail.dragons-at-work.de
SMTP_HOST="mail.dragons-at-work.de"
SMTP_PORT="465"
SMTP_USERNAME="your_email@dragons-at-work.de"
SMTP_PASSWORD="your_smtp_password"
SMTP_FROM="noreply@dragons-at-work.de"
SMTP_TO="michael@dragons-at-work.de"
EOF
echo -e "${GREEN}[OK] Created new .env file${NC}"
echo -e "${YELLOW}[EDIT] Please edit:${NC} nano $ENV_FILE"
exit 0
fi
echo -e "${GREEN}[OK] Found existing .env file${NC}"
# Check if SMTP variables already exist
smtp_username_exists=$(grep -c "^SMTP_USERNAME=" "$ENV_FILE" 2>/dev/null || echo "0")
smtp_password_exists=$(grep -c "^SMTP_PASSWORD=" "$ENV_FILE" 2>/dev/null || echo "0")
if [ "$smtp_username_exists" -gt 0 ] && [ "$smtp_password_exists" -gt 0 ]; then
echo -e "${GREEN}[OK] SMTP variables already configured${NC}"
# Load and show current values
source "$ENV_FILE"
echo -e "${YELLOW}Current SMTP User:${NC} ${SMTP_USERNAME:-NOT_SET}"
echo -e "${YELLOW}Current SMTP Password:${NC} ${SMTP_PASSWORD:+[CONFIGURED]}${SMTP_PASSWORD:-NOT_SET}"
echo ""
echo -e "${YELLOW}To update SMTP settings:${NC} nano $ENV_FILE"
exit 0
fi
# Add missing SMTP variables
echo -e "${YELLOW}Adding SMTP configuration to existing .env...${NC}"
# Add section header if not present
if ! grep -q "SMTP_" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# Furt SMTP Configuration for mail.dragons-at-work.de" >> "$ENV_FILE"
fi
# Add username if missing
if [ "$smtp_username_exists" -eq 0 ]; then
echo "SMTP_HOST=\"mail.dragons-at-work.de\"" >> "$ENV_FILE"
echo "SMTP_PORT=\"465\"" >> "$ENV_FILE"
echo "SMTP_USERNAME=\"your_email@dragons-at-work.de\"" >> "$ENV_FILE"
echo -e "${GREEN}[OK] Added SMTP_HOST, SMTP_PORT, SMTP_USERNAME${NC}"
fi
# Add password if missing
if [ "$smtp_password_exists" -eq 0 ]; then
echo "SMTP_PASSWORD=\"your_smtp_password\"" >> "$ENV_FILE"
echo "SMTP_FROM=\"noreply@dragons-at-work.de\"" >> "$ENV_FILE"
echo "SMTP_TO=\"michael@dragons-at-work.de\"" >> "$ENV_FILE"
echo -e "${GREEN}[OK] Added SMTP_PASSWORD, SMTP_FROM, SMTP_TO${NC}"
fi
echo -e "${GREEN}[OK] SMTP configuration added to .env${NC}"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Edit SMTP credentials: nano $ENV_FILE"
echo "2. Set your actual email@dragons-at-work.de in SMTP_USERNAME"
echo "3. Set your actual SMTP password in SMTP_PASSWORD"
echo "4. Test with: ./scripts/start.sh"
echo ""
echo -e "${YELLOW}Current .env content:${NC}"
echo "==================="
cat "$ENV_FILE"
echo "==================="
echo ""
echo -e "${GREEN}Ready for SMTP testing!${NC}"

View file

@ -17,10 +17,12 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}"
LUA_COMMAND=""
# Config check first
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
if [ "$(uname)" = "OpenBSD" ]; then
CONFIG_FILE="/usr/local/etc/furt/furt.conf"
PID_FILE="/var/run/furt/furt.pid"
else
CONFIG_FILE="/etc/furt/furt.conf"
PID_FILE="/var/run/furt/furt.pid"
fi
if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then
@ -87,12 +89,38 @@ cd "$PROJECT_DIR"
echo -e "${GREEN}Starting Furt...${NC}"
# PID-File cleanup function
cleanup_pid() {
if [ -f "$PID_FILE" ]; then
rm -f "$PID_FILE"
fi
}
# Service vs Interactive Detection
if [ ! -t 0 ] || [ ! -t 1 ]; then
# Service mode - Background
# Service mode - Background + PID-File
echo -e "${GREEN}Service mode: Background + PID-File${NC}"
# Start process in background
"$LUA_COMMAND" src/main.lua &
PID=$!
# Write PID-File
echo "$PID" > "$PID_FILE"
echo -e "${GREEN}Furt started (PID: $PID, PID-File: $PID_FILE)${NC}"
# Verify process is still running after short delay
sleep 1
if ! kill -0 "$PID" 2>/dev/null; then
echo -e "${RED}Error: Process died immediately${NC}"
cleanup_pid
exit 1
fi
echo -e "${GREEN}Service startup successful${NC}"
else
# Interactive mode - Foreground
# Interactive mode - Foreground (no PID-File)
echo -e "${GREEN}Interactive mode: Foreground${NC}"
exec "$LUA_COMMAND" src/main.lua
fi

View file

@ -4,7 +4,7 @@
BASE_URL="http://127.0.0.1:8080"
# Use correct API keys that match current .env
API_KEY="hugo-dev-key-change-in-production"
API_KEY="YOUR_API_KEY_HERE"
echo "⚡ Furt API Stress Test"
echo "======================"
@ -20,9 +20,9 @@ for i in {1..20}; do
response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \
"$BASE_URL/v1/auth/status")
status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then
rate_limit_remaining=$(echo "$response" | head -n -1 | jq -r '.rate_limit_remaining // "N/A"' 2>/dev/null)
echo "Request $i: ✅ 200 OK (Rate limit remaining: $rate_limit_remaining)"
@ -33,7 +33,7 @@ for i in {1..20}; do
else
echo "Request $i: ❌ $status Error"
fi
# Small delay to prevent overwhelming
sleep 0.1
done
@ -58,10 +58,10 @@ for i in {1..10}; do
-H "X-API-Key: $API_KEY" \
"$BASE_URL/health")
local_end=$(date +%s.%N)
status=$(echo "$response" | tail -c 4)
duration=$(echo "$local_end - $local_start" | bc -l)
echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i"
} &
done
@ -85,18 +85,18 @@ mail_errors=0
for i in {1..5}; do
start_time=$(date +%s.%N)
response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \
"$BASE_URL/v1/mail/send")
end_time=$(date +%s.%N)
duration=$(echo "$end_time - $start_time" | bc -l)
status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then
echo "Mail $i: ✅ 200 OK (${duration}s)"
((mail_success++))
@ -104,7 +104,7 @@ for i in {1..5}; do
echo "Mail $i: ❌ Status $status (${duration}s)"
((mail_errors++))
fi
# Delay between mail sends to be nice to SMTP server
sleep 1
done
@ -120,7 +120,7 @@ mixed_success=0
for i in {1..15}; do
((mixed_total++))
if [ $((i % 3)) -eq 0 ]; then
# Every 3rd request: auth status
endpoint="/v1/auth/status"
@ -128,20 +128,20 @@ for i in {1..15}; do
# Other requests: health check
endpoint="/health"
fi
response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \
"$BASE_URL$endpoint")
status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then
echo "Mixed $i ($endpoint): ✅ 200 OK"
((mixed_success++))
else
echo "Mixed $i ($endpoint): ❌ $status"
fi
sleep 0.2
done

View file

@ -24,8 +24,13 @@ cp -r integrations/ "$TARGET/"
[ -f "VERSION" ] && cp VERSION "$TARGET/"
[ -f ".version_history" ] && cp .version_history "$TARGET/"
# Set proper permissions
chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET"
# Set proper permissions based on operating system
if [ "$(uname)" = "OpenBSD" ]; then
chown -R root:wheel "$TARGET"
else
chown -R root:root "$TARGET"
fi
chmod -R 644 "$TARGET"
find "$TARGET" -type d -exec chmod 755 {} \;
chmod +x "$TARGET/scripts/start.sh"

View file

@ -3,8 +3,8 @@
# Test API-Key-Authentifizierung (ohne jq parse errors)
BASE_URL="http://127.0.0.1:8080"
HUGO_API_KEY="hugo-dev-key-change-in-production"
ADMIN_API_KEY="admin-dev-key-change-in-production"
HUGO_API_KEY="YOUR_API_KEY_HERE"
ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE"
INVALID_API_KEY="invalid-key-should-fail"
echo "🔐 Testing Furt API-Key Authentication"
@ -16,24 +16,24 @@ make_request() {
local url="$2"
local headers="$3"
local data="$4"
echo "Request: $method $url"
if [ -n "$headers" ]; then
echo "Headers: $headers"
fi
local response=$(curl -s $method \
${headers:+-H "$headers"} \
${data:+-d "$data"} \
-H "Content-Type: application/json" \
"$url")
local status=$(curl -s -o /dev/null -w "%{http_code}" $method \
${headers:+-H "$headers"} \
${data:+-d "$data"} \
-H "Content-Type: application/json" \
"$url")
echo "Status: $status"
echo "Response: $response" | jq '.' 2>/dev/null || echo "$response"
echo ""

2
scripts/test_modular.sh Normal file → Executable file
View file

@ -3,7 +3,7 @@
# Test der modularen Furt-Architektur
BASE_URL="http://127.0.0.1:8080"
HUGO_API_KEY="hugo-dev-key-change-in-production"
HUGO_API_KEY="YOUR_API_KEY_HERE"
echo "🧩 Testing Modular Furt Architecture"
echo "===================================="

20
scripts/test_smtp.sh Normal file → Executable file
View file

@ -36,7 +36,7 @@ else
echo "[ERROR] Validation failed"
fi
# Test 3: Invalid Email Format
# Test 3: Invalid Email Format
echo ""
echo "[3] Testing email validation..."
email_validation_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
@ -54,36 +54,36 @@ fi
# Test 4: Valid Mail Request (REAL SMTP TEST)
echo ""
echo "[4] Testing REAL mail sending..."
echo "WARNING: This will send a real email to michael@dragons-at-work.de"
echo "WARNING: This will send a real email to admin@example.com"
read -p "Continue with real mail test? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Sending real test email..."
mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
-H "Content-Type: application/json" \
-d '{
"name": "Furt Test User",
"email": "test@dragons-at-work.de",
"email": "test@example.com",
"subject": "Furt SMTP Test - Week 2 Success!",
"message": "This is a test email from the Furt Lua HTTP-Server.\n\nSMTP Integration is working!\n\nTimestamp: '$(date)'\nServer: furt-lua v1.0"
}')
echo "Response: $mail_response"
# Check for success
if echo "$mail_response" | grep -q '"success":true'; then
echo "[OK] MAIL SENT SUCCESSFULLY!"
echo "Check michael@dragons-at-work.de inbox"
echo "Check admin@example.com inbox"
# Extract request ID
request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4)
echo "Request ID: $request_id"
else
echo "[ERROR] Mail sending failed"
echo "Check server logs and SMTP credentials"
# Show error details
if echo "$mail_response" | grep -q "error"; then
error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
@ -126,7 +126,7 @@ echo "Performance: ${duration_ms}ms"
echo ""
echo "Week 2 Challenge Status:"
echo " SMTP Integration: COMPLETE"
echo " Environment Variables: CHECK .env"
echo " Environment Variables: CHECK .env"
echo " Native Lua Implementation: DONE"
echo " Production Ready: READY FOR TESTING"

View file

@ -4,7 +4,7 @@
set -e
# Detect config file location
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
if [ "$(uname)" = "OpenBSD" ]; then
CONFIG_FILE="/usr/local/etc/furt/furt.conf"
else
CONFIG_FILE="/etc/furt/furt.conf"
@ -24,12 +24,13 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then
exit 1
fi
if ! grep -q '^port\s*=' "$CONFIG_FILE"; then
# Fix: Use POSIX-compatible regex patterns
if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then
echo "Error: server port not configured"
exit 1
fi
if ! grep -q '^host\s*=' "$CONFIG_FILE"; then
if ! grep -q '^[ \t]*host[ \t]*=' "$CONFIG_FILE"; then
echo "Error: server host not configured"
exit 1
fi

View file

@ -215,7 +215,7 @@ end
function ConfigParser.load_config()
-- Try different locations based on OS
local config_paths = {
"/usr/local/etc/furt/furt.conf", -- OpenBSD/FreeBSD
"/usr/local/etc/furt/furt.conf", -- OpenBSD
"/etc/furt/furt.conf", -- Linux
"config/furt.conf", -- Development
"furt.conf" -- Current directory

View file

@ -91,9 +91,7 @@ end
function FurtServer:add_cors_headers(request)
local allowed_origins = config.cors and config.cors.allowed_origins or {
"http://localhost:1313",
"http://127.0.0.1:1313",
"https://dragons-at-work.de",
"https://www.dragons-at-work.de"
"http://127.0.0.1:1313"
}
-- Check if request has Origin header

View file

@ -1,6 +1,6 @@
-- furt-lua/src/smtp.lua
-- src/smtp.lua
-- Universal SMTP implementation with SSL compatibility
-- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter)
-- Supports both luaossl (Arch) and luasec (OpenBSD)
-- Dragons@Work Digital Sovereignty Project
local socket = require("socket")
@ -19,7 +19,7 @@ function SSLCompat:detect_ssl_library()
return "luaossl", ssl_lib
end
end
-- Try luasec
local success, ssl_lib = pcall(require, "ssl")
if success and ssl_lib then
@ -28,23 +28,23 @@ function SSLCompat:detect_ssl_library()
return "luasec", ssl_lib
end
end
return nil, "No compatible SSL library found (tried luaossl, luasec)"
end
function SSLCompat:wrap_socket(sock, options)
local ssl_type, ssl_lib = self:detect_ssl_library()
if not ssl_type then
return nil, ssl_lib -- ssl_lib contains error message
end
if ssl_type == "luaossl" then
return self:wrap_luaossl(sock, options, ssl_lib)
elseif ssl_type == "luasec" then
return self:wrap_luasec(sock, options, ssl_lib)
end
return nil, "Unknown SSL library type: " .. ssl_type
end
@ -55,18 +55,18 @@ function SSLCompat:wrap_luaossl(sock, options, ssl_lib)
protocol = "tlsv1_2",
verify = "none" -- For self-signed certs
})
if not ssl_sock then
return nil, "luaossl wrap failed: " .. (err or "unknown error")
end
-- luaossl typically does handshake automatically, but explicit is safer
local success, err = pcall(function() return ssl_sock:dohandshake() end)
if not success then
-- Some luaossl versions don't need explicit handshake
-- Continue if dohandshake doesn't exist
end
return ssl_sock, nil
end
@ -78,28 +78,28 @@ function SSLCompat:wrap_luasec(sock, options, ssl_lib)
verify = "none",
options = "all"
})
if not ssl_sock then
return nil, "luasec wrap failed: " .. (err or "unknown error")
end
-- luasec requires explicit handshake
local success, err = ssl_sock:dohandshake()
if not success then
return nil, "luasec handshake failed: " .. (err or "unknown error")
end
return ssl_sock, nil
end
-- Create SMTP instance
function SMTP:new(config)
local instance = {
server = config.smtp_server or "mail.dragons-at-work.de",
port = config.smtp_port or 465,
server = config.smtp_server,
port = config.smtp_port,
username = config.username,
password = config.password,
from_address = config.from_address or "noreply@dragons-at-work.de",
from_address = config.from_address,
use_ssl = config.use_ssl or true,
debug = config.debug or false,
ssl_compat = SSLCompat
@ -133,7 +133,7 @@ function SMTP:send_command(sock, command, expected_code)
if self.debug then
print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n"))
end
-- Only send if command is not nil (for server greeting, command is nil)
if command then
local success, err = sock:send(command .. "\r\n")
@ -141,16 +141,16 @@ function SMTP:send_command(sock, command, expected_code)
return false, "Failed to send command: " .. (err or "unknown error")
end
end
local response, err = sock:receive()
if not response then
return false, "Failed to receive response: " .. (err or "unknown error")
end
if self.debug then
print("SMTP RSP: " .. response)
end
-- Handle multi-line responses (like EHLO)
local full_response = response
while response:match("^%d%d%d%-") do
@ -163,12 +163,12 @@ function SMTP:send_command(sock, command, expected_code)
end
full_response = full_response .. "\n" .. response
end
local code = response:match("^(%d+)")
if expected_code and code ~= tostring(expected_code) then
return false, "Unexpected response: " .. full_response
end
return true, full_response
end
@ -179,38 +179,38 @@ function SMTP:connect()
if not sock then
return false, "Failed to create socket: " .. (err or "unknown error")
end
-- Set timeout
sock:settimeout(30)
-- Connect to server
local success, err = sock:connect(self.server, self.port)
if not success then
return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error")
end
-- Wrap with SSL for port 465 using compatibility layer
if self.use_ssl and self.port == 465 then
local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
mode = "client",
protocol = "tlsv1_2"
})
if not ssl_sock then
sock:close()
return false, "Failed to establish SSL connection: " .. (err or "unknown error")
end
sock = ssl_sock
end
-- Read server greeting
local success, response = self:send_command(sock, nil, 220)
if not success then
sock:close()
return false, "SMTP server greeting failed: " .. response
end
return sock, nil
end
@ -219,64 +219,96 @@ function SMTP:send_email(to_address, subject, message, from_name)
if not self.username or not self.password then
return false, "SMTP username or password not configured"
end
-- Connect to server
local sock, err = self:connect()
if not sock then
return false, err
end
local function cleanup_and_fail(error_msg)
sock:close()
return false, error_msg
end
-- EHLO command
local success, response = self:send_command(sock, "EHLO furt-lua", 250)
if not success then
return cleanup_and_fail("EHLO failed: " .. response)
end
-- STARTTLS hinzufügen für Port 587
if self.port == 587 and self.use_ssl then
-- STARTTLS command
local success, response = self:send_command(sock, "STARTTLS", 220)
if not success then
return cleanup_and_fail("STARTTLS failed: " .. response)
end
-- Upgrade connection to SSL
local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
mode = "client",
protocol = "tlsv1_2"
})
if not ssl_sock then
return cleanup_and_fail("SSL upgrade failed: " .. err)
end
sock = ssl_sock
-- EHLO again over encrypted connection
local success, response = self:send_command(sock, "EHLO furt-lua", 250)
if not success then
return cleanup_and_fail("EHLO after STARTTLS failed: " .. response)
end
end
-- AUTH LOGIN
local success, response = self:send_command(sock, "AUTH LOGIN", 334)
if not success then
return cleanup_and_fail("AUTH LOGIN failed: " .. response)
end
-- Send username (base64 encoded)
local username_b64 = self:base64_encode(self.username)
local success, response = self:send_command(sock, username_b64, 334)
if not success then
return cleanup_and_fail("Username authentication failed: " .. response)
end
-- Send password (base64 encoded)
local password_b64 = self:base64_encode(self.password)
local success, response = self:send_command(sock, password_b64, 235)
if not success then
return cleanup_and_fail("Password authentication failed: " .. response)
end
-- MAIL FROM
local mail_from = "MAIL FROM:<" .. self.from_address .. ">"
local success, response = self:send_command(sock, mail_from, 250)
if not success then
return cleanup_and_fail("MAIL FROM failed: " .. response)
end
-- RCPT TO
local rcpt_to = "RCPT TO:<" .. to_address .. ">"
local success, response = self:send_command(sock, rcpt_to, 250)
if not success then
return cleanup_and_fail("RCPT TO failed: " .. response)
end
-- DATA command
local success, response = self:send_command(sock, "DATA", 354)
if not success then
return cleanup_and_fail("DATA command failed: " .. response)
end
-- Generate unique Message-ID
-- Extract domain from configured from_address
local hostname = self.from_address:match("@(.+)") or self.server
local message_id = string.format("<%d.%d@%s>", os.time(), math.random(10000), hostname)
-- Build email message
local display_name = from_name or "Furt Contact Form"
local email_content = string.format(
@ -284,7 +316,10 @@ function SMTP:send_email(to_address, subject, message, from_name)
"To: <%s>\r\n" ..
"Subject: %s\r\n" ..
"Date: %s\r\n" ..
"Message-ID: %s\r\n" ..
"MIME-Version: 1.0\r\n" ..
"Content-Type: text/plain; charset=UTF-8\r\n" ..
"Content-Transfer-Encoding: 8bit\r\n" ..
"\r\n" ..
"%s\r\n" ..
".",
@ -293,19 +328,20 @@ function SMTP:send_email(to_address, subject, message, from_name)
to_address,
subject,
os.date("%a, %d %b %Y %H:%M:%S %z"),
message_id,
message
)
-- Send email content
local success, response = self:send_command(sock, email_content, 250)
if not success then
return cleanup_and_fail("Email sending failed: " .. response)
end
-- QUIT
self:send_command(sock, "QUIT", 221)
sock:close()
return true, "Email sent successfully"
end