Infosec403

mail_전체흐름.html

40.7 KB · text/html · 업로드 2026-08-26 23:25

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Host A에서 Host B까지 — 메일 전송 메커니즘</title>
  <meta name="description" content="Roundcube, Postfix, Dovecot, vhost DBMS, DNS와 Maildir/mbox를 포함한 메일 전송 과정">
  <style>
    :root {
      --ink:#111827; --muted:#667085; --soft:#98a2b3; --line:#d8dee8;
      --paper:#ffffff; --wash:#f7f8fa; --a:#2563eb; --a-bg:#f3f7ff;
      --b:#15803d; --b-bg:#f3faf5; --shared:#c2410c; --shared-bg:#fff7f2;
      --db:#6d28d9; --db-bg:#f7f3ff; --store:#0f766e; --store-bg:#f0fdfa;
    }
    *{box-sizing:border-box}
    html{scroll-behavior:smooth;scroll-snap-type:y mandatory}
    body{margin:0;background:#e8ecf1;color:var(--ink);font-family:Arial,"Noto Sans KR","Malgun Gothic",sans-serif}
    button{font:inherit}
    .progress{position:fixed;left:0;right:0;top:0;height:4px;background:#e4e7ec;z-index:20}
    .progress span{display:block;height:100%;width:9.09%;background:var(--a);transition:width .2s ease}
    .slide{position:relative;min-height:100svh;overflow:hidden;scroll-snap-align:start;background:var(--paper);padding:58px clamp(34px,5.5vw,86px) 72px}
    .slide:nth-of-type(even){background:var(--wash)}
    .num{position:absolute;right:clamp(28px,4vw,58px);top:34px;color:var(--soft);font:700 12px/1 monospace;letter-spacing:.08em}
    .kicker{margin:0 0 11px;color:var(--a);font:800 12px/1.2 monospace;letter-spacing:.14em}
    .kicker.green{color:var(--b)} .kicker.orange{color:var(--shared)} .kicker.purple{color:var(--db)}
    h1,h2,h3,p{overflow-wrap:break-word}
    h1{max-width:980px;margin:0;font-size:clamp(46px,5.2vw,76px);line-height:1.08;letter-spacing:-.05em}
    h2{margin:0;font-size:clamp(34px,3.5vw,52px);line-height:1.12;letter-spacing:-.04em}
    h3{margin:0}
    .subtitle{margin:15px 0 0;color:var(--muted);font-size:clamp(16px,1.5vw,21px);line-height:1.55}
    .foot{position:absolute;left:clamp(34px,5.5vw,86px);right:clamp(34px,5.5vw,86px);bottom:30px;margin:0;padding-top:13px;border-top:1px solid var(--line);color:var(--muted);font-size:12px;line-height:1.45}
    .legend{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
    .legend span,.pill{display:inline-flex;align-items:center;gap:6px;padding:6px 9px;border:1px solid var(--line);border-radius:7px;background:#fff;color:#475467;font-size:11px;font-weight:700}
    .dot{width:8px;height:8px;border-radius:2px;background:var(--a)}
    .dot.b{background:var(--b)} .dot.shared{background:var(--shared)} .dot.db{background:var(--db)} .dot.store{background:var(--store)}
    .node{min-width:0;border:2px solid var(--line);border-radius:12px;background:#fff;padding:16px;text-align:center}
    .node strong{display:block;font-size:15px;line-height:1.25}
    .node small{display:block;margin-top:5px;color:var(--muted);font-size:11px;line-height:1.35}
    .node.a{border-color:#9dbcf8;background:var(--a-bg)} .node.b{border-color:#9bd0aa;background:var(--b-bg)}
    .node.shared{border-color:#efb18d;background:var(--shared-bg)} .node.db{border-color:#c9b4f5;background:var(--db-bg)}
    .node.store{border-color:#8dd5ce;background:var(--store-bg)}
    .node .tag{display:inline-block;margin-bottom:8px;color:var(--soft);font:800 9px/1 monospace;letter-spacing:.08em}
    .edge{position:relative;min-width:72px;display:grid;align-content:center;justify-items:center;gap:7px;color:#475467}
    .edge b{display:block;text-align:center;color:var(--a);font:800 10px/1.35 monospace;white-space:nowrap}
    .edge.green b{color:var(--b)} .edge.orange b{color:var(--shared)} .edge.purple b{color:var(--db)} .edge.teal b{color:var(--store)}
    .edge i{position:relative;display:block;width:100%;height:2px;background:currentColor;font-style:normal}
    .edge i::after{content:"";position:absolute;right:-1px;top:-4px;border-left:8px solid currentColor;border-top:5px solid transparent;border-bottom:5px solid transparent}
    .edge.two i::before{content:"";position:absolute;left:-1px;top:-4px;border-right:8px solid currentColor;border-top:5px solid transparent;border-bottom:5px solid transparent}
    .callout{border-left:4px solid var(--a);padding:10px 14px;background:#f8fafc;color:#475467;font-size:13px;line-height:1.55}
    .callout.orange{border-left-color:var(--shared)} .callout.green{border-left-color:var(--b)}
    code{font-family:Consolas,"SFMono-Regular",monospace}

    /* 1 */
    .hero-route{display:grid;grid-template-columns:1fr 110px 1.25fr 130px 1.25fr 110px 1fr;align-items:center;gap:12px;margin-top:68px}
    .hero-route .node{min-height:118px;display:grid;align-content:center}
    .hero-route .edge b{font-size:11px}
    .hero-note{margin-top:34px;max-width:820px;padding:13px 15px;border-radius:8px;background:#f2f4f7;color:#475467;font-size:13px;line-height:1.5}

    /* 2 */
    .system-map{display:grid;grid-template-columns:1fr 165px 1fr;gap:20px;align-items:center;margin-top:36px}
    .domain{border:2px solid var(--line);border-radius:14px;background:#fff;padding:18px}
    .domain.a{border-top:6px solid var(--a)} .domain.b{border-top:6px solid var(--b)}
    .domain-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:13px}
    .domain-head h3{font-size:21px}.domain-head span{color:var(--muted);font-size:10px;font-weight:700}
    .service-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    .service{min-height:66px;border:1px solid var(--line);border-radius:8px;padding:10px;background:#fff}
    .service b{display:block;font-size:12px;margin-bottom:4px}.service small{display:block;color:var(--muted);font-size:10px;line-height:1.35}
    .service.wide{grid-column:1/-1}.dns-box{border:2px solid #efb18d;border-radius:12px;background:var(--shared-bg);padding:18px;text-align:center}
    .dns-box b{display:block;color:var(--shared);font-size:15px;margin-bottom:7px}.dns-box small{color:var(--muted);font-size:10px;line-height:1.5}
    .dns-links{margin-top:10px;color:var(--shared);font:800 10px/1.45 monospace}

    /* 3 */
    .hop-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 16px;margin-top:28px}
    .hop{display:grid;grid-template-columns:38px 1fr 90px 1fr;align-items:center;gap:9px;border:1px solid var(--line);border-radius:10px;background:#fff;padding:10px}
    .hop-no{width:30px;height:30px;display:grid;place-items:center;border-radius:50%;background:var(--ink);color:#fff;font:800 11px/1 monospace}
    .hop .mini{min-width:0}.hop .mini b{display:block;font-size:11px;line-height:1.3}.hop .mini small{display:block;color:var(--muted);font-size:9px;margin-top:3px;line-height:1.3}
    .hop .edge b{font-size:8.5px}.hop .edge{min-width:65px}.hop.storage-hop{border-color:#9bd0aa;background:var(--b-bg)}

    /* 4 */
    .lane-board{display:grid;grid-template-columns:1fr;gap:16px;margin-top:34px}
    .lane{display:grid;grid-template-columns:150px 1fr;gap:18px;align-items:center;border:1px solid var(--line);border-radius:11px;background:#fff;padding:15px 18px}
    .lane-title b{display:block;color:var(--a);font-size:14px}.lane-title small{display:block;margin-top:5px;color:var(--muted);font-size:10px;line-height:1.35}
    .lane-flow{display:grid;grid-template-columns:1fr 100px 1fr 100px 1fr;align-items:center;gap:8px}
    .queue{margin-top:18px;display:grid;grid-template-columns:1fr 1fr;gap:12px}
    .queue .callout b{color:var(--ink)}

    /* 5 */
    .dns-flow{display:grid;grid-template-columns:1fr 105px 1fr 105px 1fr;align-items:center;gap:12px;margin-top:52px}
    .dns-flow .node{min-height:126px;display:grid;align-content:center}
    .dns-answer{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:28px}
    .record{border:1px solid var(--line);border-radius:9px;background:#fff;padding:15px}
    .record span{display:block;color:var(--shared);font:800 10px/1 monospace;margin-bottom:8px}.record code{font-size:13px;color:#344054}

    /* 6 */
    .smtp-transfer{display:grid;grid-template-columns:1fr 165px 1fr;align-items:center;gap:16px;margin-top:46px}
    .smtp-server{border:2px solid var(--line);border-radius:13px;background:#fff;padding:20px}
    .smtp-server.a{border-top:6px solid var(--a)} .smtp-server.b{border-top:6px solid var(--b)}
    .smtp-server h3{font-size:20px}.smtp-server p{color:var(--muted);font-size:11px;line-height:1.45}
    .spool{margin-top:13px;border:1px dashed #98a2b3;border-radius:8px;padding:10px;background:#f9fafb}
    .spool b{display:block;font-size:11px}.spool code{display:block;margin-top:4px;color:#667085;font-size:9px}
    .smtp-dialog{display:grid;gap:6px;margin-top:10px}
    .smtp-dialog code{display:block;padding:5px 8px;border-radius:5px;background:#f2f4f7;color:#344054;font-size:10px}
    .smtp-center .edge{margin-bottom:17px}.smtp-center .callout{text-align:left}

    /* 7 */
    .receive-map{display:grid;grid-template-columns:1fr 105px 1fr 105px 1fr;align-items:center;gap:12px;margin-top:38px}
    .receive-map .node{min-height:112px;display:grid;align-content:center}
    .db-branch{display:grid;grid-template-columns:1fr 115px 1fr;align-items:center;gap:12px;width:min(720px,100%);margin:22px auto 0}
    .db-fields{display:flex;justify-content:center;gap:6px;flex-wrap:wrap;margin-top:7px}.db-fields span{padding:4px 6px;border-radius:4px;background:#ede9fe;color:#5b21b6;font:700 9px/1 monospace}
    .decision{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:20px}.decision div{border:1px solid var(--line);border-radius:8px;padding:11px 13px;font-size:11px;color:#475467}.decision b{margin-right:7px}.ok b{color:var(--b)}.fail b{color:#b42318}

    /* 8 */
    .read-layout{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:36px}
    .read-card{border:2px solid var(--line);border-radius:13px;background:#fff;padding:20px}
    .read-card.primary{border-top:6px solid var(--b)}.read-card.alt{border-top:6px solid #7c3aed}
    .read-card h3{font-size:19px}.read-card>p{margin:7px 0 18px;color:var(--muted);font-size:11px;line-height:1.45}
    .read-flow{display:grid;grid-template-columns:1fr 92px 1fr 92px 1fr;align-items:center;gap:7px}.read-flow.short{grid-template-columns:1fr 115px 1fr}
    .read-flow .node{padding:11px}.read-flow .node strong{font-size:12px}.read-flow .edge b{font-size:8px}
    .read-note{margin-top:18px;padding:11px;border-radius:7px;background:#f8fafc;color:#475467;font-size:11px;line-height:1.45}

    /* 9 */
    .store-table{display:grid;grid-template-columns:140px 1fr 1fr 1.2fr;gap:1px;margin-top:34px;border:1px solid var(--line);background:var(--line);border-radius:10px;overflow:hidden}
    .cell{min-height:58px;background:#fff;padding:12px;display:grid;align-content:center}.cell.head{background:#f2f4f7;color:#475467;font-size:10px;font-weight:800}.cell b{font-size:12px}.cell small{margin-top:4px;color:var(--muted);font-size:10px;line-height:1.35}.cell code{font-size:10px;color:#344054}
    .maildir-tree{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:22px}
    .folder{border:1px solid #8dd5ce;border-radius:9px;background:var(--store-bg);padding:13px}.folder b{display:block;color:var(--store);font:800 13px/1 monospace}.folder small{display:block;margin-top:6px;color:#475467;font-size:10px;line-height:1.4}
    .pathbar{margin-top:16px;padding:11px 14px;border-radius:7px;background:#111827;color:#f9fafb;font:600 11px/1.5 monospace}

    /* 10 */
    .compare{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:34px}
    .format{border:2px solid var(--line);border-radius:13px;background:#fff;padding:21px}.format.recommended{border-top:6px solid var(--store)}
    .format-head{display:flex;justify-content:space-between;align-items:flex-start}.format h3{font-size:24px}.format .badge{padding:5px 7px;border-radius:5px;background:var(--store-bg);color:var(--store);font-size:9px;font-weight:800}
    .format-example{margin:17px 0;padding:14px;border-radius:8px;background:#f8fafc;font:600 10px/1.6 monospace;color:#344054;min-height:82px}
    .format ul{list-style:none;margin:0;padding:0}.format li{display:grid;grid-template-columns:95px 1fr;gap:10px;padding:9px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.4}.format li b{color:#475467}.format li span{color:var(--muted)}

    /* 11 */
    .port-table{display:grid;grid-template-columns:1.15fr 1.15fr .9fr 1.7fr;gap:1px;margin-top:28px;border:1px solid var(--line);background:var(--line);border-radius:10px;overflow:hidden}
    .port-table .cell{min-height:42px;padding:9px 12px}.port-table .head{min-height:36px}.port{font:800 11px/1.2 monospace;color:var(--a)}
    .final-flow{display:grid;grid-template-columns:1fr 75px 1fr 75px 1fr 75px 1fr;align-items:center;gap:7px;margin-top:20px}.final-flow .node{padding:11px}.final-flow .node strong{font-size:12px}.final-flow .edge b{font-size:8px}

    .deck-nav{position:fixed;right:20px;bottom:18px;z-index:30;display:flex;align-items:center;gap:6px;padding:6px;border-radius:10px;background:#111827;box-shadow:0 8px 25px rgb(17 24 39 / 18%)}
    .deck-nav button{width:38px;height:36px;border:0;border-radius:7px;background:#344054;color:#fff;cursor:pointer;font-size:18px}.deck-nav button:disabled{opacity:.28;cursor:default}
    .deck-nav span{min-width:64px;text-align:center;color:#98a2b3;font:700 10px/1 monospace}.deck-nav span b{color:#fff}

    @media(max-width:900px){
      html{scroll-snap-type:none}.slide{min-height:100svh;height:auto;overflow:visible;padding:55px 26px 86px}.foot{position:static;margin-top:24px}.hero-route{grid-template-columns:1fr 75px 1fr;row-gap:18px}.hero-route>:nth-child(4){display:none}.hero-route>:nth-child(n+5){grid-row:2}.hero-route>:nth-child(5){grid-column:1}.hero-route>:nth-child(6){grid-column:2}.hero-route>:nth-child(7){grid-column:3}.system-map{grid-template-columns:1fr}.dns-box{order:2}.domain.b{order:3}.hop-grid{grid-template-columns:1fr}.lane{grid-template-columns:1fr}.lane-flow{grid-template-columns:1fr 70px 1fr 70px 1fr}.dns-flow{grid-template-columns:1fr 70px 1fr;row-gap:18px}.dns-flow>:nth-child(4){display:none}.dns-flow>:nth-child(5){grid-column:3}.smtp-transfer{grid-template-columns:1fr}.smtp-center{display:grid;grid-template-columns:120px 1fr;gap:14px;align-items:center}.receive-map{grid-template-columns:1fr 75px 1fr}.receive-map>:nth-child(4){display:none}.receive-map>:nth-child(5){grid-column:3}.read-layout,.compare{grid-template-columns:1fr}.store-table{grid-template-columns:110px 1fr 1fr}.store-table .cell:nth-child(4n){display:none}.port-table{grid-template-columns:1fr .8fr 1.4fr}.port-table .cell:nth-child(4n+2){display:none}.final-flow{grid-template-columns:1fr 55px 1fr 55px 1fr}.final-flow>:nth-child(6),.final-flow>:nth-child(7){display:none}
    }
    @media(max-width:600px){h1{font-size:42px}h2{font-size:32px}.service-grid{grid-template-columns:1fr}.service.wide{grid-column:auto}.hop{grid-template-columns:34px 1fr 70px 1fr}.lane-flow,.read-flow{grid-template-columns:1fr}.lane-flow .edge,.read-flow .edge{height:50px}.lane-flow .edge i,.read-flow .edge i{width:2px;height:28px}.lane-flow .edge i::after,.read-flow .edge i::after{right:-4px;top:auto;bottom:-1px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:8px solid currentColor;border-bottom:0}.queue,.dns-answer,.decision,.maildir-tree{grid-template-columns:1fr}.db-branch{grid-template-columns:1fr}.db-branch .edge{height:45px}.read-flow.short{grid-template-columns:1fr}.store-table{grid-template-columns:95px 1fr}.store-table .cell:nth-child(3n){display:none}.port-table{grid-template-columns:1fr 1fr}.port-table .cell:nth-child(3n){display:none}.format li{grid-template-columns:80px 1fr}.hero-route{margin-top:42px}.deck-nav{right:12px;bottom:12px}}
    @media print{html{scroll-snap-type:none}body{background:#fff}.slide{page-break-after:always;min-height:100vh}.deck-nav,.progress{display:none}}
    @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
  </style>
</head>
<body>
  <div class="progress" aria-hidden="true"><span></span></div>

  <section class="slide" id="slide-1" aria-label="1장: 표지">
    <span class="num">01 / 11</span>
    <p class="kicker">MAIL DELIVERY MECHANISM</p>
    <h1>Host A가 보낸 메일은<br>Host B까지 어떻게 가는가?</h1>
    <p class="subtitle">alice@naver.com → bob@gmail.com · 인증서와 보안 협상은 생략</p>
    <div class="hero-route">
      <div class="node a"><span class="tag">USER</span><strong>Host A</strong><small>메일 작성</small></div>
      <div class="edge"><b>HTTP(S)<br>TCP 80 / 443</b><i></i></div>
      <div class="node a"><span class="tag">DOMAIN A</span><strong>naver.com 메일 시스템</strong><small>Roundcube · Postfix · Dovecot</small></div>
      <div class="edge orange"><b>SMTP<br>TCP 25</b><i></i></div>
      <div class="node b"><span class="tag">DOMAIN B</span><strong>gmail.com 메일 시스템</strong><small>Postfix · Dovecot · Maildir</small></div>
      <div class="edge green"><b>HTTP(S)<br>TCP 80 / 443</b><i></i></div>
      <div class="node b"><span class="tag">USER</span><strong>Host B</strong><small>메일 열람</small></div>
    </div>
    <div class="hero-note"><b>범위:</b> 메일 제출 → DNS 조회 → 서버 간 전송 → 로컬 배달 → 우편함 저장 → 사용자 열람</div>
    <p class="foot">교육용 자체 구축 구조를 naver.com / gmail.com 이름으로 설명합니다. 실제 네이버·구글의 내부 구현을 뜻하지 않습니다.</p>
  </section>

  <section class="slide" id="slide-2" aria-label="2장: 시스템 구성">
    <span class="num">02 / 11</span>
    <p class="kicker">SYSTEM BOUNDARY</p>
    <h2>두 메일 시스템과 외부 DNS</h2>
    <p class="subtitle">Roundcube, Postfix, Dovecot, vhost DBMS를 서로 다른 역할로 분리한다.</p>
    <div class="system-map">
      <article class="domain a">
        <div class="domain-head"><h3>naver.com</h3><span>DOMAIN A</span></div>
        <div class="service-grid">
          <div class="service"><b>Roundcube WAS</b><small>웹 메일 UI · IMAP/SMTP 클라이언트</small></div>
          <div class="service"><b>Postfix SMTP</b><small>메일 제출 · 큐 · 외부 MTA 전송</small></div>
          <div class="service"><b>Dovecot</b><small>LMTP 최종 배달 · IMAP/POP3 열람</small></div>
          <div class="service"><b>vhost DBMS</b><small>가상 도메인 · 계정 · 별칭 · 경로</small></div>
          <div class="service wide"><b>Mail Storage</b><small>사용자별 Maildir 또는 mbox · 메일 본문과 첨부 저장</small></div>
        </div>
      </article>
      <div class="dns-box"><b>외부 DNS</b><small>gmail.com의 MX 조회<br>MX 호스트의 A/AAAA 조회</small><div class="dns-links">DNS · UDP/TCP 53</div></div>
      <article class="domain b">
        <div class="domain-head"><h3>gmail.com</h3><span>DOMAIN B</span></div>
        <div class="service-grid">
          <div class="service"><b>Roundcube WAS</b><small>웹 메일 UI · IMAP/SMTP 클라이언트</small></div>
          <div class="service"><b>Postfix SMTP</b><small>외부 수신 · 큐 · 메일 제출</small></div>
          <div class="service"><b>Dovecot</b><small>LMTP 최종 배달 · IMAP/POP3 열람</small></div>
          <div class="service"><b>vhost DBMS</b><small>수신 도메인 · 사용자 · 저장 경로</small></div>
          <div class="service wide"><b>Mail Storage</b><small>bob의 최종 우편함 · 이 예시는 Maildir 사용</small></div>
        </div>
      </article>
    </div>
    <div class="legend"><span><i class="dot"></i>발신 영역</span><span><i class="dot b"></i>수신 영역</span><span><i class="dot shared"></i>외부 공통 인프라</span><span><i class="dot db"></i>계정 메타데이터</span></div>
    <p class="foot">vhost DBMS에는 메일 본문이 아니라 도메인·사용자·별칭·쿼터·홈/메일 경로 같은 메타데이터를 저장한다.</p>
  </section>

  <section class="slide" id="slide-3" aria-label="3장: 전체 흐름">
    <span class="num">03 / 11</span>
    <p class="kicker orange">END-TO-END</p>
    <h2>메일 한 통의 8개 홉</h2>
    <p class="subtitle">각 화살표가 실제 연결 주체, 프로토콜, 포트를 보여준다.</p>
    <div class="hop-grid">
      <div class="hop"><span class="hop-no">1</span><div class="mini"><b>Host A</b><small>웹 브라우저</small></div><div class="edge"><b>HTTP(S)<br>80 / 443</b><i></i></div><div class="mini"><b>Roundcube A</b><small>메일 작성 UI</small></div></div>
      <div class="hop"><span class="hop-no">2</span><div class="mini"><b>Roundcube A</b><small>완성된 메시지</small></div><div class="edge"><b>SMTP SUBMIT<br>TCP 587</b><i></i></div><div class="mini"><b>Postfix A</b><small>발신 큐에 수용</small></div></div>
      <div class="hop"><span class="hop-no">3</span><div class="mini"><b>Postfix A</b><small>gmail.com 목적지</small></div><div class="edge orange two"><b>DNS<br>UDP/TCP 53</b><i></i></div><div class="mini"><b>외부 DNS</b><small>MX · A/AAAA 응답</small></div></div>
      <div class="hop"><span class="hop-no">4</span><div class="mini"><b>Postfix A</b><small>발신 MTA</small></div><div class="edge orange"><b>SMTP<br>TCP 25</b><i></i></div><div class="mini"><b>Postfix B</b><small>수신 MTA</small></div></div>
      <div class="hop"><span class="hop-no">5</span><div class="mini"><b>Postfix B</b><small>수신자 확인</small></div><div class="edge purple two"><b>SQL<br>3306 / 5432</b><i></i></div><div class="mini"><b>vhost DBMS B</b><small>bob 계정 · 경로</small></div></div>
      <div class="hop"><span class="hop-no">6</span><div class="mini"><b>Postfix B</b><small>로컬 배달 요청</small></div><div class="edge green"><b>LMTP · UNIX 소켓<br>또는 TCP 24</b><i></i></div><div class="mini"><b>Dovecot B</b><small>최종 배달 담당</small></div></div>
      <div class="hop storage-hop"><span class="hop-no">7</span><div class="mini"><b>Dovecot B</b><small>메시지 저장</small></div><div class="edge teal"><b>파일시스템 I/O<br>포트 없음</b><i></i></div><div class="mini"><b>bob의 Maildir</b><small>tmp → new → cur</small></div></div>
      <div class="hop"><span class="hop-no">8</span><div class="mini"><b>Roundcube B</b><small>받은편지함 요청</small></div><div class="edge green"><b>IMAP<br>TCP 143</b><i></i></div><div class="mini"><b>Dovecot B</b><small>메일 목록 · 본문 반환</small></div></div>
    </div>
    <p class="foot">Host B의 브라우저는 Dovecot에 직접 접속하지 않는다. 브라우저는 Roundcube와 HTTP(S)로, Roundcube는 Dovecot과 IMAP으로 통신한다.</p>
  </section>

  <section class="slide" id="slide-4" aria-label="4장: 발신 측">
    <span class="num">04 / 11</span>
    <p class="kicker">DOMAIN A · SEND</p>
    <h2>Host A에서 메일을 제출한다</h2>
    <p class="subtitle">Roundcube는 화면을 제공하고, 실제 외부 전송은 Postfix가 맡는다.</p>
    <div class="lane-board">
      <div class="lane">
        <div class="lane-title"><b>① 웹 메일 접속</b><small>브라우저가 Roundcube 화면을 사용</small></div>
        <div class="lane-flow"><div class="node a"><strong>Host A</strong><small>Browser</small></div><div class="edge"><b>HTTP(S)<br>80 / 443</b><i></i></div><div class="node a"><strong>Roundcube A</strong><small>WAS · 메시지 작성</small></div><div class="edge purple two"><b>SQL<br>3306 / 5432</b><i></i></div><div class="node db"><strong>Roundcube DB</strong><small>환경설정 · 주소록 · 세션</small></div></div>
      </div>
      <div class="lane">
        <div class="lane-title"><b>② 발신 제출</b><small>Roundcube가 완성된 메일을 Postfix로 전달</small></div>
        <div class="lane-flow"><div class="node a"><strong>Roundcube A</strong><small>From · To · 본문 · 첨부</small></div><div class="edge"><b>SMTP SUBMISSION<br>TCP 587</b><i></i></div><div class="node a"><strong>Postfix A</strong><small>발신 큐 생성</small></div><div class="edge purple two"><b>SQL<br>3306 / 5432</b><i></i></div><div class="node db"><strong>vhost DBMS A</strong><small>발신 도메인 · 계정 · 별칭</small></div></div>
      </div>
      <div class="lane">
        <div class="lane-title"><b>③ 보낸편지함 사본</b><small>Roundcube가 별도로 Sent 폴더에 추가</small></div>
        <div class="lane-flow"><div class="node a"><strong>Roundcube A</strong><small>Sent 폴더 APPEND</small></div><div class="edge teal"><b>IMAP<br>TCP 143</b><i></i></div><div class="node a"><strong>Dovecot A</strong><small>발신자 우편함 접근</small></div><div class="edge teal"><b>파일시스템 I/O<br>포트 없음</b><i></i></div><div class="node store"><strong>alice의 Maildir</strong><small>Sent 사본 저장</small></div></div>
      </div>
    </div>
    <div class="queue"><div class="callout"><b>Postfix 큐</b> 상대 서버가 받을 때까지 메일을 임시 보관하고 실패하면 재시도한다.</div><div class="callout green"><b>Sent 사본</b> 수신 서버로 가는 원본과 별개로 발신자 우편함에 저장된다.</div></div>
    <p class="foot">Roundcube DB와 vhost DBMS는 논리적으로 구분했다. 운영 환경에서는 같은 DBMS 인스턴스를 공유할 수도 있다.</p>
  </section>

  <section class="slide" id="slide-5" aria-label="5장: DNS 조회">
    <span class="num">05 / 11</span>
    <p class="kicker orange">EXTERNAL DNS</p>
    <h2>어느 서버로 보낼지 MX로 찾는다</h2>
    <p class="subtitle">DNS는 메일을 운반하지 않고, 목적지 메일 서버의 이름과 IP를 알려준다.</p>
    <div class="dns-flow">
      <div class="node a"><span class="tag">MTA</span><strong>Postfix A</strong><small>수신자 도메인: gmail.com</small></div>
      <div class="edge orange two"><b>DNS QUERY<br>UDP/TCP 53</b><i></i></div>
      <div class="node shared"><span class="tag">RESOLVER</span><strong>재귀 DNS</strong><small>MX와 A/AAAA를 순서대로 조회</small></div>
      <div class="edge orange two"><b>DNS QUERY<br>UDP/TCP 53</b><i></i></div>
      <div class="node shared"><span class="tag">AUTHORITATIVE</span><strong>gmail.com 권한 DNS</strong><small>도메인의 공식 레코드 응답</small></div>
    </div>
    <div class="dns-answer">
      <div class="record"><span>1 · MX RESPONSE</span><code>gmail.com. MX 10 mail.gmail.com.</code></div>
      <div class="record"><span>2 · A / AAAA RESPONSE</span><code>mail.gmail.com. A 203.0.113.20</code></div>
    </div>
    <div class="callout orange" style="margin-top:22px"><b>결과:</b> Postfix A는 응답받은 IP의 TCP 25번 포트로 연결한다. MX 우선순위가 여러 개면 낮은 숫자부터 시도한다.</div>
    <p class="foot">일반 DNS 질의는 UDP 53을 우선 사용하고, 응답이 크거나 재시도·영역 전송 등의 경우 TCP 53을 사용할 수 있다.</p>
  </section>

  <section class="slide" id="slide-6" aria-label="6장: SMTP 서버 간 전송">
    <span class="num">06 / 11</span>
    <p class="kicker orange">MTA TO MTA</p>
    <h2>Postfix A에서 Postfix B로 전송한다</h2>
    <p class="subtitle">서버 간 메일 전달은 SMTP TCP 25가 담당한다.</p>
    <div class="smtp-transfer">
      <article class="smtp-server a"><h3>Postfix A</h3><p>발신 MTA · alice@naver.com</p><div class="spool"><b>발신 큐 · 임시 저장</b><code>/var/spool/postfix/...</code></div><div class="smtp-dialog"><code>EHLO mail.naver.com</code><code>MAIL FROM:&lt;alice@naver.com&gt;</code><code>RCPT TO:&lt;bob@gmail.com&gt;</code><code>DATA ...</code></div></article>
      <div class="smtp-center"><div class="edge orange"><b>SMTP<br>TCP 25</b><i></i></div><div class="callout orange">Postfix B가 <b>250 OK</b>로 수락하면 A는 해당 큐 항목을 제거한다.</div></div>
      <article class="smtp-server b"><h3>Postfix B</h3><p>수신 MTA · gmail.com</p><div class="spool"><b>수신 큐 · 임시 저장</b><code>/var/spool/postfix/...</code></div><div class="smtp-dialog"><code>250 mail.gmail.com</code><code>250 Sender OK</code><code>250 Recipient OK</code><code>250 Message accepted</code></div></article>
    </div>
    <div class="callout" style="margin-top:22px"><b>중요:</b> 이 시점의 성공은 “gmail.com의 Postfix가 메일을 인수했다”는 뜻이다. bob의 최종 우편함 저장은 다음 단계인 LMTP에서 완료된다.</div>
    <p class="foot">인증, TLS, 스팸 필터, DKIM/SPF/DMARC 처리는 흐름을 단순화하기 위해 생략했다.</p>
  </section>

  <section class="slide" id="slide-7" aria-label="7장: 수신 측 로컬 배달">
    <span class="num">07 / 11</span>
    <p class="kicker green">DOMAIN B · LOCAL DELIVERY</p>
    <h2>수신자를 확인하고 우편함에 넣는다</h2>
    <p class="subtitle">Postfix가 받은 메일을 Dovecot LMTP에 넘기면, Dovecot이 최종 파일을 만든다.</p>
    <div class="receive-map">
      <div class="node b"><span class="tag">MTA</span><strong>Postfix B</strong><small>수신 큐의 메시지</small></div>
      <div class="edge green"><b>LMTP · UNIX 소켓<br>포트 없음<br><span style="font-weight:500">(분리 시 TCP 24)</span></b><i></i></div>
      <div class="node b"><span class="tag">DELIVERY</span><strong>Dovecot LMTP</strong><small>사용자별 최종 배달</small></div>
      <div class="edge teal"><b>파일시스템 I/O<br>포트 없음</b><i></i></div>
      <div class="node store"><span class="tag">FINAL STORAGE</span><strong>bob의 Maildir</strong><small>tmp → new → cur</small></div>
    </div>
    <div class="db-branch">
      <div class="node b"><strong>Postfix / Dovecot</strong><small>수신 도메인과 사용자 조회</small></div>
      <div class="edge purple two"><b>MySQL TCP 3306<br>또는 PostgreSQL TCP 5432</b><i></i></div>
      <div class="node db"><strong>vhost DBMS B</strong><small>메일 본문은 저장하지 않음</small><div class="db-fields"><span>domain</span><span>mailbox</span><span>alias</span><span>quota</span><span>mail_path</span></div></div>
    </div>
    <div class="decision"><div class="ok"><b>정상 계정</b>LMTP 성공 응답 → Postfix B 큐 제거</div><div class="fail"><b>없는 계정</b>수신 거부 또는 반송 처리</div></div>
    <p class="foot">같은 서버라면 Postfix와 Dovecot 사이 LMTP는 보통 <code>/var/spool/postfix/private/dovecot-lmtp</code> 같은 UNIX 소켓을 사용한다.</p>
  </section>

  <section class="slide" id="slide-8" aria-label="8장: 메일 열람">
    <span class="num">08 / 11</span>
    <p class="kicker green">READING MAIL</p>
    <h2>Host B가 저장된 메일을 읽는다</h2>
    <p class="subtitle">웹 메일은 IMAP을 사용하고, 별도 메일 클라이언트는 IMAP 또는 POP3를 선택한다.</p>
    <div class="read-layout">
      <article class="read-card primary"><h3>웹 메일 · Roundcube</h3><p>브라우저와 Dovecot 사이를 Roundcube가 중계한다.</p><div class="read-flow"><div class="node b"><strong>Host B</strong><small>Browser</small></div><div class="edge green two"><b>HTTP(S)<br>80 / 443</b><i></i></div><div class="node b"><strong>Roundcube B</strong><small>WAS</small></div><div class="edge green two"><b>IMAP<br>TCP 143</b><i></i></div><div class="node b"><strong>Dovecot B</strong><small>Maildir 읽기</small></div></div><div class="read-note">목록·폴더·읽음·삭제 상태는 서버 우편함 기준으로 관리된다.</div></article>
      <article class="read-card alt"><h3>별도 메일 클라이언트</h3><p>Outlook·Thunderbird 같은 프로그램이 Dovecot에 직접 연결한다.</p><div class="read-flow short"><div class="node"><strong>메일 클라이언트</strong><small>Host B의 앱</small></div><div class="edge purple two"><b>IMAP TCP 143<br>또는 POP3 TCP 110</b><i></i></div><div class="node b"><strong>Dovecot B</strong><small>IMAP/POP3 서비스</small></div></div><div class="read-note"><b>IMAP:</b> 서버와 상태 동기화 · <b>POP3:</b> 메시지를 클라이언트로 내려받기</div></article>
    </div>
    <div class="callout green" style="margin-top:20px"><b>답장 전송:</b> Roundcube B → Postfix B는 다시 SMTP Submission TCP 587을 사용한다.</div>
    <p class="foot">인증서 설명은 생략했으므로 기본 포트 143/110을 표시했다. 보안 전용 포트는 IMAPS 993, POP3S 995이다.</p>
  </section>

  <section class="slide" id="slide-9" aria-label="9장: 메일 저장 위치">
    <span class="num">09 / 11</span>
    <p class="kicker green">WHERE IS THE MESSAGE?</p>
    <h2>메일은 단계마다 저장 위치가 달라진다</h2>
    <p class="subtitle">큐는 임시 저장, Maildir/mbox는 사용자 우편함의 최종 저장이다.</p>
    <div class="store-table">
      <div class="cell head">단계</div><div class="cell head">담당</div><div class="cell head">저장 성격</div><div class="cell head">대표 위치 / 동작</div>
      <div class="cell"><b>발신 제출 직후</b></div><div class="cell"><b>Postfix A</b><small>SMTP 587로 수용</small></div><div class="cell"><b>임시 큐</b><small>상대 서버 수락까지</small></div><div class="cell"><code>/var/spool/postfix/...</code><small>실패 시 재시도</small></div>
      <div class="cell"><b>보낸편지함 사본</b></div><div class="cell"><b>Dovecot A</b><small>IMAP APPEND</small></div><div class="cell"><b>최종 우편함</b><small>alice의 Sent</small></div><div class="cell"><code>/var/vmail/naver.com/alice/Maildir/.Sent/</code></div>
      <div class="cell"><b>수신 직후</b></div><div class="cell"><b>Postfix B</b><small>SMTP 25로 수용</small></div><div class="cell"><b>임시 큐</b><small>LMTP 완료까지</small></div><div class="cell"><code>/var/spool/postfix/...</code><small>로컬 배달 대기</small></div>
      <div class="cell"><b>LMTP 완료</b></div><div class="cell"><b>Dovecot B</b><small>파일시스템 I/O</small></div><div class="cell"><b>최종 우편함</b><small>bob의 INBOX</small></div><div class="cell"><code>/var/vmail/gmail.com/bob/Maildir/</code></div>
    </div>
    <div class="pathbar">/var/vmail/gmail.com/bob/Maildir/</div>
    <div class="maildir-tree"><div class="folder"><b>tmp/</b><small>저장 중인 임시 파일. 쓰기가 끝나기 전 단계.</small></div><div class="folder"><b>new/</b><small>배달이 끝났지만 아직 확인되지 않은 새 메일.</small></div><div class="folder"><b>cur/</b><small>클라이언트가 확인한 메일과 상태 플래그.</small></div></div>
    <p class="foot">Maildir에서는 메일 한 통이 파일 하나다. 파일명과 위치 변경으로 읽음 등 상태를 반영한다.</p>
  </section>

  <section class="slide" id="slide-10" aria-label="10장: mbox와 Maildir 비교">
    <span class="num">10 / 11</span>
    <p class="kicker">MAILBOX FORMAT</p>
    <h2>mbox와 Maildir 저장 방식 비교</h2>
    <p class="subtitle">차이는 “메일 여러 통을 한 파일에 넣는가, 한 통씩 파일로 나누는가”이다.</p>
    <div class="compare">
      <article class="format"><div class="format-head"><h3>mbox</h3><span class="badge" style="background:#f2f4f7;color:#475467">전통 방식</span></div><div class="format-example">/var/mail/bob<br>└─ 하나의 큰 파일 안에<br>&nbsp;&nbsp;&nbsp;메일 1 + 메일 2 + 메일 3 ...</div><ul><li><b>저장 단위</b><span>메일함 하나 = 파일 하나</span></li><li><b>쓰기</b><span>큰 파일 끝에 메시지를 이어 붙임</span></li><li><b>동시 접근</b><span>파일 잠금이 중요하고 경합 가능</span></li><li><b>장점</b><span>파일 수가 적고 순차 읽기·아카이브가 단순</span></li><li><b>주의</b><span>파일이 커지며 수정·복구 영향 범위가 큼</span></li></ul></article>
      <article class="format recommended"><div class="format-head"><h3>Maildir</h3><span class="badge">이 예시의 선택</span></div><div class="format-example">Maildir/<br>├─ tmp/9F...<br>├─ new/A3...<br>└─ cur/B7...:2,S</div><ul><li><b>저장 단위</b><span>메일 한 통 = 파일 하나</span></li><li><b>쓰기</b><span>tmp에 쓴 뒤 new/cur로 원자적 이동</span></li><li><b>동시 접근</b><span>메일별 파일이라 경합과 손상 범위가 작음</span></li><li><b>장점</b><span>동시 접속·가상 사용자 환경에 유리</span></li><li><b>주의</b><span>파일·inode 수가 많고 파일시스템 성능 영향</span></li></ul></article>
    </div>
    <div class="callout green" style="margin-top:20px"><b>결론:</b> 이 자료의 가상 사용자 구조는 <code>/var/vmail/&lt;domain&gt;/&lt;user&gt;/Maildir/</code> 형태를 사용한다.</div>
    <p class="foot">Dovecot 문서는 mbox를 전통적 호환·아카이브 용도로 설명하며, 활성 프로덕션 저장에는 Maildir 같은 현대적 형식을 권장한다.</p>
  </section>

  <section class="slide" id="slide-11" aria-label="11장: 포트와 전체 요약">
    <span class="num">11 / 11</span>
    <p class="kicker orange">PORT & ROLE RECAP</p>
    <h2>프로토콜·포트 한 장 정리</h2>
    <div class="port-table">
      <div class="cell head">출발 → 도착</div><div class="cell head">프로토콜</div><div class="cell head">포트</div><div class="cell head">역할</div>
      <div class="cell"><b>Browser ↔ Roundcube</b></div><div class="cell"><b>HTTP / HTTPS</b></div><div class="cell"><span class="port">TCP 80 / 443</span></div><div class="cell"><small>웹 메일 화면 요청·응답</small></div>
      <div class="cell"><b>Roundcube → Postfix</b></div><div class="cell"><b>SMTP Submission</b></div><div class="cell"><span class="port">TCP 587</span></div><div class="cell"><small>사용자가 작성한 메일 제출</small></div>
      <div class="cell"><b>Postfix → 외부 DNS</b></div><div class="cell"><b>DNS</b></div><div class="cell"><span class="port">UDP/TCP 53</span></div><div class="cell"><small>수신 도메인의 MX·A/AAAA 조회</small></div>
      <div class="cell"><b>Postfix A → Postfix B</b></div><div class="cell"><b>SMTP</b></div><div class="cell"><span class="port">TCP 25</span></div><div class="cell"><small>메일 서버 간 전송</small></div>
      <div class="cell"><b>Postfix → Dovecot</b></div><div class="cell"><b>LMTP</b></div><div class="cell"><span class="port">UNIX 소켓 / TCP 24</span></div><div class="cell"><small>로컬 사용자의 최종 우편함 배달</small></div>
      <div class="cell"><b>Roundcube → Dovecot</b></div><div class="cell"><b>IMAP</b></div><div class="cell"><span class="port">TCP 143</span></div><div class="cell"><small>웹 메일 목록·본문·폴더·상태 동기화</small></div>
      <div class="cell"><b>Mail client → Dovecot</b></div><div class="cell"><b>POP3</b></div><div class="cell"><span class="port">TCP 110</span></div><div class="cell"><small>메일을 클라이언트로 내려받기</small></div>
      <div class="cell"><b>Mail services ↔ vhost DBMS</b></div><div class="cell"><b>MySQL / PostgreSQL</b></div><div class="cell"><span class="port">TCP 3306 / 5432</span></div><div class="cell"><small>가상 도메인·사용자·별칭·경로 조회</small></div>
    </div>
    <div class="final-flow"><div class="node a"><strong>Roundcube A</strong><small>작성</small></div><div class="edge"><b>587</b><i></i></div><div class="node a"><strong>Postfix A</strong><small>DNS 53 조회</small></div><div class="edge orange"><b>SMTP 25</b><i></i></div><div class="node b"><strong>Postfix B</strong><small>수신</small></div><div class="edge green"><b>LMTP</b><i></i></div><div class="node store"><strong>Dovecot → Maildir</strong><small>최종 저장</small></div></div>
    <p class="foot">전체 핵심: Roundcube는 UI, Postfix는 SMTP 운송, DNS는 목적지 탐색, vhost DBMS는 계정 메타데이터, Dovecot은 LMTP 저장과 IMAP/POP3 열람을 맡는다.</p>
  </section>

  <nav class="deck-nav" aria-label="슬라이드 이동"><button id="prev" aria-label="이전 슬라이드" disabled>←</button><span><b id="current">01</b> / 11</span><button id="next" aria-label="다음 슬라이드">→</button></nav>
  <script>
    (()=>{
      const slides=[...document.querySelectorAll('.slide')];
      const prev=document.querySelector('#prev'),next=document.querySelector('#next');
      const currentLabel=document.querySelector('#current'),bar=document.querySelector('.progress span');
      let current=0;
      function update(index){current=Math.max(0,Math.min(slides.length-1,index));currentLabel.textContent=String(current+1).padStart(2,'0');bar.style.width=`${((current+1)/slides.length)*100}%`;prev.disabled=current===0;next.disabled=current===slides.length-1}
      function go(index){const nextIndex=Math.max(0,Math.min(slides.length-1,index));slides[nextIndex].scrollIntoView({behavior:'smooth'});update(nextIndex)}
      prev.addEventListener('click',()=>go(current-1));next.addEventListener('click',()=>go(current+1));
      const observer=new IntersectionObserver(entries=>{const visible=entries.filter(e=>e.isIntersecting).sort((a,b)=>b.intersectionRatio-a.intersectionRatio)[0];if(visible)update(slides.indexOf(visible.target))},{threshold:[.45,.65]});slides.forEach(s=>observer.observe(s));
      addEventListener('keydown',e=>{if(['ArrowRight','ArrowDown','PageDown'].includes(e.key)){e.preventDefault();go(current+1)}if(['ArrowLeft','ArrowUp','PageUp'].includes(e.key)){e.preventDefault();go(current-1)}if(e.key==='Home')go(0);if(e.key==='End')go(slides.length-1)});
      update(0);
    })();
  </script>
</body>
</html>