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