Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- geoip
- ip차단
- sftp
- 파일질라
- 오블완
- SMTP설정
- SSL인증서
- Apache
- postfix
- 리눅스서버
- 리눅스명령어
- centos7
- DNS
- FTP
- 시스템관리
- 웹서버
- 서버보안
- oops-firwall
- iptables
- 서버운영
- 국가IP차단
- 리눅스
- https
- 서버방화벽
- ubuntu
- FileZilla
- CentOS
- linux
- 서버관리
- 티스토리챌린지
Archives
- Today
- Total
운영중입니다
Postfix + PHPMailer 메일 발송 시스템을 웹 UI로 사용하는 방법 본문
https://https443.tistory.com/42
이전 글에서는 Ubuntu 24.04 환경에서 Postfix와 PHPMailer를 이용하여
CLI(Command Line) 기반으로 동일한 메일을 여러 사용자에게 발송하는 방법을 정리하였습니다.
이번 글에서는 해당 내용을 확장하여
웹 브라우저에서 직접 수신자 목록 관리, 제목 수정, 테스트 발송, 전체 발송까지 가능한
간단한 메일 발송 UI를 만드는 방법을 정리합니다.
1. 테스트 환경
| 항목 | 내용 |
| OS | Ubuntu 24.04.2 LTS |
| Apache | Apache/2.4.58 |
| PHP | PHP 8.3 |
| 메일 발송 | Postfix |
| 메일 라이브러리 | PHPMailer |
#Apache 기본 웹 경로는 일반적으로 아래와 같습니다.
/var/www/html
#서버마다 다를 수 있으므로 실제 환경에 맞게 확인이 필요합니다.
2. 생성
2.1 기존 CLI 메일 발송 프로젝트 복사
#이전 글에서 사용한 /root/mailtest 디렉토리를 웹에서 접근 가능한 경로로 복사합니다.
mkdir -p /var/www/html/mailtest
cp -a /root/mailtest/* /var/www/html/mailtest/
2.2 웹 수정 가능하도록 권한 설정
chown -R www-data:www-data /var/www/html/mailtest
chmod 755 /var/www/html/mailtest
chmod 644 /var/www/html/mailtest/maillist.txt
chmod 644 /var/www/html/mailtest/send_mail_sent.json 2>/dev/null
# 644로 설정 후 실행이 안될 경우 666을 설정하나 666은 소유자, 그룹, 전체
# 모두 읽고 쓰기가 가능하므로, 임시로 사용하거나 644를 사용합니다.
2.3 index.php 생성
#메일 발송 UI는 단일 PHP 파일로 구성하였습니다.
cd /var/www/html/mailtest
vim index.php
<?php
session_start();
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/PHPMailer/src/Exception.php';
require __DIR__ . '/PHPMailer/src/PHPMailer.php';
require __DIR__ . '/PHPMailer/src/SMTP.php';
date_default_timezone_set("Asia/Seoul");
/*
|--------------------------------------------------------------------------
| 관리자 / SMTP 설정
|--------------------------------------------------------------------------
*/
$adminPassword = "비빌번호 설정";
$smtpHost = "사용 메일 서버";
$smtpPort = 465;
$smtpUser = "사용 메일 계정";
$smtpPass = "사용 메일 계정 비밀번호";
$fromEmail = "발송 주소";
$fromName = "발송자명";
/*
|--------------------------------------------------------------------------
| 파일 설정
|--------------------------------------------------------------------------
*/
$mailListFile = __DIR__ . "/maillist.txt";
$sentFile = __DIR__ . "/send_mail_sent.json";
$lockFile = __DIR__ . "/send_mail.lock";
/*
|--------------------------------------------------------------------------
| 로그인 처리
|--------------------------------------------------------------------------
*/
if (isset($_POST['login_password'])) {
if ($_POST['login_password'] === $adminPassword) {
$_SESSION['login'] = true;
header("Location: index.php");
exit;
} else {
$loginError = "비밀번호가 올바르지 않습니다.";
}
}
if (isset($_GET['logout'])) {
session_destroy();
header("Location: index.php");
exit;
}
if (empty($_SESSION['login'])) {
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>메일 발송 관리자</title>
<style>
body { font-family: Arial, "Malgun Gothic", sans-serif; background:#f3f4f6; }
.login-box { width:360px; margin:120px auto; background:#fff; padding:30px; border-radius:12px; box-shadow:0 4px 14px rgba(0,0,0,.1); }
input { width:100%; padding:12px; box-sizing:border-box; margin-top:10px; }
button { width:100%; padding:12px; margin-top:15px; background:#1d4ed8; color:#fff; border:0; border-radius:6px; cursor:pointer; }
.error { color:red; margin-top:10px; }
</style>
</head>
<body>
<div class="login-box">
<h2>메일 발송 관리자</h2>
<form method="post">
<input type="password" name="login_password" placeholder="관리자 비밀번호">
<button type="submit">로그인</button>
</form>
<?php if (!empty($loginError)) echo '<div class="error">'.$loginError.'</div>'; ?>
</div>
</body>
</html>
<?php
exit;
}
/*
|--------------------------------------------------------------------------
| 기본값
|--------------------------------------------------------------------------
*/
$currentMailList = file_exists($mailListFile) ? file_get_contents($mailListFile) : "";
$subject = $_POST['subject'] ?? "";
$body = $_POST['body'] ?? "";
$message = "";
$results = [];
/*
|--------------------------------------------------------------------------
| 수신자 목록 저장
|--------------------------------------------------------------------------
*/
if (isset($_POST['save_maillist'])) {
file_put_contents($mailListFile, trim($_POST['maillist']) . "\n");
$currentMailList = file_get_contents($mailListFile);
$message = "수신자 목록이 저장되었습니다.";
}
/*
|--------------------------------------------------------------------------
| 발송 기록 초기화
|--------------------------------------------------------------------------
*/
if (isset($_POST['reset_sent'])) {
if (file_exists($sentFile)) {
unlink($sentFile);
}
$message = "발송 기록이 초기화되었습니다.";
}
/*
|--------------------------------------------------------------------------
| 발송 함수
|--------------------------------------------------------------------------
*/
function sendMailOne(
$to,
$subject,
$body,
$smtpHost,
$smtpPort,
$smtpUser,
$smtpPass,
$fromEmail,
$fromName
) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = $smtpHost;
$mail->SMTPAuth = true;
$mail->Username = $smtpUser;
$mail->Password = $smtpPass;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = $smtpPort;
$mail->CharSet = "UTF-8";
$mail->setFrom($fromEmail, $fromName);
$mail->addAddress($to);
$mail->Subject = $subject;
$mail->isHTML(true);
$mail->Body = nl2br($body);
$mail->AltBody = strip_tags($body);
$mail->send();
}
/*
|--------------------------------------------------------------------------
| 발송 처리
|--------------------------------------------------------------------------
*/
if (isset($_POST['send_test']) || isset($_POST['send_all'])) {
$subject = trim($_POST['subject'] ?? "");
$body = trim($_POST['body'] ?? "");
if ($subject === "") {
$results[] = "[FAIL] 제목이 비어 있습니다.";
}
if ($body === "") {
$results[] = "[FAIL] 본문이 비어 있습니다.";
}
if (empty($results)) {
$lockHandle = fopen($lockFile, "c");
if (!$lockHandle) {
$results[] = "[FAIL] 잠금 파일 생성 실패";
} elseif (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
$results[] = "[FAIL] 이미 발송 작업이 실행 중입니다.";
} else {
$sentList = [];
if (file_exists($sentFile)) {
$sentList = json_decode(file_get_contents($sentFile), true);
if (!is_array($sentList)) {
$sentList = [];
}
}
if (isset($_POST['send_test'])) {
$targetList = [trim($_POST['test_email'])];
} else {
$targetList = file($mailListFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$targetList = array_unique($targetList);
}
foreach ($targetList as $to) {
$to = trim($to);
if (!$to) {
continue;
}
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
$results[] = "[SKIP] 잘못된 이메일 주소: {$to}";
continue;
}
if (isset($_POST['send_all']) && isset($sentList[$to])) {
$results[] = "[SKIP] 이미 발송 완료: {$to}";
continue;
}
try {
sendMailOne(
$to,
$subject,
$body,
$smtpHost,
$smtpPort,
$smtpUser,
$smtpPass,
$fromEmail,
$fromName
);
if (isset($_POST['send_all'])) {
$sentList[$to] = [
"sent_at" => date("Y-m-d H:i:s"),
"subject" => $subject
];
file_put_contents(
$sentFile,
json_encode($sentList, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
LOCK_EX
);
}
$results[] = "[OK] 발송 완료: {$to}";
} catch (Exception $e) {
$results[] = "[FAIL] {$to} / " . $e->getMessage();
}
sleep(2);
}
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
}
}
}
/*
|--------------------------------------------------------------------------
| 발송 완료 건수
|--------------------------------------------------------------------------
*/
$sentCount = 0;
if (file_exists($sentFile)) {
$tmp = json_decode(file_get_contents($sentFile), true);
if (is_array($tmp)) {
$sentCount = count($tmp);
}
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>메일 발송 관리자</title>
<style>
body {
font-family: Arial, "Malgun Gothic", sans-serif;
background:#f4f6f9;
margin:0;
padding:30px;
color:#111;
}
.container {
max-width:1200px;
margin:0 auto;
}
.header {
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:20px;
}
.card {
background:#fff;
padding:24px;
border-radius:14px;
box-shadow:0 4px 14px rgba(0,0,0,.08);
margin-bottom:20px;
}
h1, h2 {
margin-top:0;
}
textarea,
input[type="text"],
input[type="email"] {
width:100%;
box-sizing:border-box;
padding:12px;
border:1px solid #ccc;
border-radius:8px;
font-family:Consolas, "Malgun Gothic", sans-serif;
}
textarea {
min-height:220px;
}
.body-area {
min-height:420px;
}
button {
padding:12px 18px;
border:0;
border-radius:8px;
cursor:pointer;
font-weight:bold;
}
.btn-blue { background:#1d4ed8; color:#fff; }
.btn-green { background:#15803d; color:#fff; }
.btn-red { background:#dc2626; color:#fff; }
.btn-gray { background:#555; color:#fff; }
.result {
background:#111827;
color:#fff;
padding:15px;
border-radius:10px;
white-space:pre-line;
}
.notice {
background:#ecfdf5;
border:1px solid #bbf7d0;
padding:12px;
border-radius:8px;
margin-bottom:15px;
}
.row {
display:grid;
grid-template-columns:1fr 1fr;
gap:20px;
}
.small {
color:#666;
font-size:13px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>메일 발송 관리자</h1>
<a href="?logout=1">로그아웃</a>
</div>
<?php if ($message): ?>
<div class="notice"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<div class="card">
<h2>발송 상태</h2>
<p>발송 완료 기록: <b><?php echo $sentCount; ?></b>건</p>
<form method="post" onsubmit="return confirm('발송 기록을 초기화하시겠습니까? 이미 보낸 주소도 다시 발송 가능해집니다.');">
<button type="submit" name="reset_sent" class="btn-red">발송 기록 초기화</button>
</form>
</div>
<div class="card">
<h2>수신자 목록</h2>
<p class="small">한 줄에 이메일 주소 1개씩 입력합니다.</p>
<form method="post">
<textarea name="maillist"><?php echo htmlspecialchars($currentMailList); ?></textarea>
<br><br>
<button type="submit" name="save_maillist" class="btn-blue">수신자 목록 저장</button>
</form>
</div>
<form method="post" id="sendForm">
<div class="card">
<h2>메일 제목</h2>
<input type="text" name="subject" value="<?php echo htmlspecialchars($subject); ?>" placeholder="메일 제목을 입력하세요">
</div>
<div class="card">
<h2>메일 본문</h2>
<p class="small">줄바꿈은 메일에서도 그대로 적용됩니다. HTML 태그를 직접 입력해도 됩니다.</p>
<textarea name="body" class="body-area" placeholder="발송할 메일 내용을 입력하세요"><?php echo htmlspecialchars($body); ?></textarea>
</div>
<div class="row">
<div class="card">
<h2>테스트 발송</h2>
<input type="email" name="test_email" placeholder="테스트 받을 이메일 주소">
<br><br>
<button type="submit" name="send_test" class="btn-green">테스트 발송</button>
</div>
<div class="card">
<h2>전체 발송</h2>
<p class="small">수신자 목록에 저장된 주소 기준으로 1명씩 개별 발송됩니다.</p>
<button type="submit" name="send_all" class="btn-red" onclick="return confirm('전체 발송하시겠습니까?');">전체 발송</button>
</div>
</div>
</form>
<?php if (!empty($results)): ?>
<div class="card">
<h2>발송 결과</h2>
<div class="result"><?php echo htmlspecialchars(implode("\n", $results)); ?></div>
</div>
<?php endif; ?>
</div>
</body>
</html>
3. 주요 기능
| 기능 | 설명 |
| 로그인 기능 | 관리자 비밀번호 인증 |
| 수신자 목록 관리 | 웹에서 직접 수정 가능 |
| 테스트 발송 | 특정 주소로 테스트 가능 |
| 전체 발송 | 목록 기준 개별 발송 |
| 발송 기록 저장 | JSON 파일 저장 |
| 중복 발송 방지 | 이미 보낸 주소 제외 |
| 발송 기록 초기화 | 재발송 가능 |
| lock 처리 | 중복 실행 방지 |
| 제목 수정 | 웹 UI에서 즉시 변경 가능 |
| HTML 메일 지원 | 줄바꿈 및 HTML 적용 가능 |
3.1 로그인 화면

# 비밀번호는 위에 기입 된 코드 내부에서 직접 설정합니다.
$adminPassword = "비밀번호 설정";
# 실제 운영 환경에서는 아래 사항도 함께 적용하는 것이 좋습니다.
#HTTPS 적용, 관리자 IP 제한, 세션 타임아웃, .htaccess 접근 제한 등
3.2 메일 발송 관리자 화면
로그인 후에는 메일 발송 관리 화면이 출력됩니다. 현재 UI에서는 아래 기능을 웹에서 직접 처리 할 수 있습니다.
| 기능 | |
| 1 | 수신자 목록 저장 |
| 2 | 제목 수정 |
| 3 | 본문 수정 |
| 4 | 테스트 발송 |
| 5 | 전체 발송 |
| 6 | 발송 결과 확인 |
| 7 | 발송 기록 초기화 |


3.3 발송 기록 저장 방식, 중복 실행 방지(lock), 스팸 판정 완화
#발송 완료 주소는 JSON 파일로 저장
# 해당 코드로 이미 발송한 주소를 자동 제외할 수 있습니다.
{
"user@example.com": {
"sent_at": "2026-05-18 13:00:00",
"subject": "안내 메일"
}
}
#---------------------------------------------------------------------------------------------
#발송 중 중복 실행되는 상황을 막기 위해 lock 파일 기반 처리도 추가하였습니다.
# * 락(lock) 방식은 특정 작업이 실행 중일 때 잠금 상태를 만들어 동일 작업이 동시에 중복 실행되지 않도록 막는 처리 방식입니다.
$lockFile = __DIR__ . "/send_mail.lock";
#---------------------------------------------------------------------------------------------
#메일 서버에서 너무 빠르게 대량 발송하면 스팸 판정을 받을 가능성이 높아질 수 있습니다.
#이에 발송 간 간단한 딜레이를 추가하였습니다.
sleep(2);
4. 마무리
기존 CLI 기반의 Postfix + PHPMailer 메일 발송 시스템을 웹 UI 형태로 확장할 경우,
관리 편의성과 사용성이 크게 향상되며, PHP 단일 파일만으로도 비교적 간단하게 구성할 수 있다는 장점이 있습니다.
다만 일부 권한 설정 및 SMTP 정보 관리 방식에 따라 보안상 취약점이 발생할 수 있으므로,
운영 환경에서는 권한 관리 및 접근 제어 설정에 주의가 필요합니다.
'etc' 카테고리의 다른 글
| 그누보드5 (Gnuboard5) 설치 방법 정리 (0) | 2026.05.21 |
|---|---|
| FileZilla (파일질라) 구형 서버 접속 오류 해결 방법 (0) | 2026.05.20 |
| CentOS7 Remi Repository(remi.repo) 설정 (0) | 2026.05.16 |
| 구글 검색 결과에 표시되는 파비콘(Favicon) 변경 방법 정리 (0) | 2026.05.15 |
| Microsoft Edge의 IE 모드 사용 방법 (0) | 2026.02.20 |