Implement Contact Form with Python FastAPI backend and mailcow SMTP integration

This commit is contained in:
Wind
2026-02-15 06:16:52 +09:00
parent 7e5f452449
commit 998d733eb5
6 changed files with 241 additions and 6 deletions

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.11-slim
WORKDIR /app
# 필요한 패키지 설치
RUN pip install --no-cache-dir fastapi uvicorn pydantic pydantic[email]
# 앱 파일 복사
COPY mail_contact.py .
# 포트 8001 노출
EXPOSE 8001
# 앱 실행
CMD ["python", "mail_contact.py"]

View File

@@ -20,3 +20,22 @@ services:
restart: unless-stopped
volumes:
- ./html:/usr/share/nginx/html:ro
# 3. Contact Form Email Handler
mail-contact:
build: .
container_name: hanmo-mail-contact
restart: unless-stopped
ports:
- '8001:8001'
environment:
- PYTHONUNBUFFERED=1
command: python mail_contact.py
networks:
- default
- mailcow-network
networks:
mailcow-network:
external: true
name: mailcowdockerized_mailcow-network

View File

@@ -46,3 +46,78 @@ mobileNavLinks.forEach(link => {
mobileMenu.classList.add('translate-x-full');
});
});
// Contact form handler
const contactSubmitBtn = document.getElementById('contact-submit');
const contactForm = {
name: document.getElementById('contact-name'),
email: document.getElementById('contact-email'),
company: document.getElementById('contact-company'),
message: document.getElementById('contact-message')
};
const contactStatus = document.getElementById('contact-status');
if (contactSubmitBtn) {
contactSubmitBtn.addEventListener('click', async (e) => {
e.preventDefault();
// 폼 데이터 수집
const formData = {
name: contactForm.name.value,
email: contactForm.email.value,
company: contactForm.company.value,
message: contactForm.message.value
};
// 로딩 상태 표시
contactSubmitBtn.disabled = true;
contactSubmitBtn.textContent = 'Sending...';
contactStatus.classList.add('hidden');
try {
// Python FastAPI 엔드포인트로 요청
const response = await fetch('http://localhost:8001/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData),
mode: 'cors',
credentials: 'omit'
});
const result = await response.json();
// 응답 처리
contactStatus.classList.remove('hidden');
if (response.ok && result.success !== false) {
// 성공 메시지
contactStatus.classList.remove('bg-red-900/50', 'text-red-200');
contactStatus.classList.add('bg-green-900/50', 'text-green-200');
contactStatus.textContent = result.message;
// 폼 초기화
contactForm.name.value = '';
contactForm.email.value = '';
contactForm.company.value = '';
contactForm.message.value = '';
} else {
// 오류 메시지
contactStatus.classList.remove('bg-green-900/50', 'text-green-200');
contactStatus.classList.add('bg-red-900/50', 'text-red-200');
contactStatus.textContent = result.detail || result.message || 'Failed to send message';
}
} catch (error) {
// 네트워크 오류
contactStatus.classList.remove('hidden', 'bg-green-900/50', 'text-green-200');
contactStatus.classList.add('bg-red-900/50', 'text-red-200');
contactStatus.textContent = 'Error: Could not send message. Please try again later.';
console.error('Contact form error:', error);
} finally {
// 버튼 상태 복구
contactSubmitBtn.disabled = false;
contactSubmitBtn.textContent = 'Send Message';
}
});
}

View File

@@ -399,12 +399,13 @@
<div class="max-w-2xl mx-auto">
<div class="grid grid-cols-1 gap-5">
<div class="grid grid-cols-2 gap-5">
<input type="text" placeholder="Full Name" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
<input type="email" placeholder="Email Address" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
<input type="text" id="contact-name" placeholder="Full Name" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
<input type="email" id="contact-email" placeholder="Email Address" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
</div>
<input type="text" placeholder="Company Name" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
<textarea rows="5" placeholder="Describe your project requirements..." class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors resize-none font-light"></textarea>
<button class="bg-blue-600 hover:bg-blue-700 text-white py-5 rounded-2xl font-bold transition-all hover:-translate-y-0.5 font-industrial text-xs tracking-widest uppercase shadow-2xl">Send Message</button>
<input type="text" id="contact-company" placeholder="Company Name" class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors font-light">
<textarea rows="5" id="contact-message" placeholder="Describe your project requirements..." class="bg-white/5 border border-white/10 text-white placeholder-slate-500 px-6 py-5 rounded-2xl focus:outline-none focus:border-blue-500 transition-colors resize-none font-light"></textarea>
<button id="contact-submit" class="bg-blue-600 hover:bg-blue-700 text-white py-5 rounded-2xl font-bold transition-all hover:-translate-y-0.5 font-industrial text-xs tracking-widest uppercase shadow-2xl">Send Message</button>
<div id="contact-status" class="hidden p-5 rounded-2xl text-center font-light"></div>
</div>
</div>
</div>

View File

@@ -1 +1 @@
{"cpu_temp": "40", "nvme_temp": "35", "uptime_days": 2, "last_update": "05:53:01"}
{"cpu_temp": "43", "nvme_temp": "35", "uptime_days": 2, "last_update": "06:16:01"}

125
mail_contact.py Normal file
View File

@@ -0,0 +1,125 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, validator
from fastapi.middleware.cors import CORSMiddleware
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os
from typing import Optional
app = FastAPI()
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class ContactForm(BaseModel):
name: str
email: EmailStr
company: str
message: str
@validator('name')
def name_not_empty(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Full Name is required')
return v.strip()
@validator('company')
def company_not_empty(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Company Name is required')
return v.strip()
@validator('message')
def message_valid(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Project description is required')
if len(v) > 5000:
raise ValueError('Message is too long (max 5000 characters)')
return v.strip()
@app.post("/api/contact")
async def send_contact_email(form: ContactForm):
"""Contact form email handler"""
try:
# SMTP 설정
smtp_server = "mailcowdockerized_postfix-mailcow_1" # Docker 네트워크 내 호스트명
smtp_port = 25 # 일반 포트 (요청할 때)
# 메시지 구성
msg = MIMEMultipart('alternative')
msg['Subject'] = f"[Contact Form] {form.name} - {form.company}"
msg['From'] = form.email
msg['To'] = "windpacer@hanmocnn.co.kr"
# 텍스트 본문
text_body = f"""
New contact form submission:
Name: {form.name}
Email: {form.email}
Company: {form.company}
Message:
{form.message}
---
Submitted from Contact Form
"""
# HTML 본문
html_body = f"""
<html>
<body>
<h3>New contact form submission:</h3>
<p><strong>Name:</strong> {form.name}</p>
<p><strong>Email:</strong> {form.email}</p>
<p><strong>Company:</strong> {form.company}</p>
<p><strong>Message:</strong></p>
<pre>{form.message}</pre>
<hr>
<p><em>Submitted from Hanmo Contact Form</em></p>
</body>
</html>
"""
part1 = MIMEText(text_body, 'plain')
part2 = MIMEText(html_body, 'html')
msg.attach(part1)
msg.attach(part2)
# SMTP 연결 및 메일 전송
with smtplib.SMTP(smtp_server, smtp_port, timeout=10) as server:
server.sendmail(
form.email,
"windpacer@hanmocnn.co.kr",
msg.as_string()
)
return {
"success": True,
"message": "Your message has been sent successfully. We will get back to you soon."
}
except Exception as e:
import traceback
print(f"Error sending email: {str(e)}")
print(traceback.format_exc())
raise HTTPException(
status_code=500,
detail="Failed to send email. Please try again later."
)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)