Implement Contact Form with Python FastAPI backend and mailcow SMTP integration
This commit is contained in:
15
Dockerfile
Normal file
15
Dockerfile
Normal 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"]
|
||||||
@@ -20,3 +20,22 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./html:/usr/share/nginx/html:ro
|
- ./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
|
||||||
|
|||||||
@@ -46,3 +46,78 @@ mobileNavLinks.forEach(link => {
|
|||||||
mobileMenu.classList.add('translate-x-full');
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -399,12 +399,13 @@
|
|||||||
<div class="max-w-2xl mx-auto">
|
<div class="max-w-2xl mx-auto">
|
||||||
<div class="grid grid-cols-1 gap-5">
|
<div class="grid grid-cols-1 gap-5">
|
||||||
<div class="grid grid-cols-2 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="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" 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="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>
|
</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">
|
<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" 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>
|
<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 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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
125
mail_contact.py
Normal 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)
|
||||||
Reference in New Issue
Block a user