Excel VBA Outlook 초안 저장: 첨부 재확인과 재실행 차단 예제

여러 메일을 만들다 두 번째 행에서 멈췄을 때, 다시 실행하면 첫 번째 초안이 중복될 수 있습니다. 이 글은 자동 발송을 구현하지 않습니다. 두 건의 가상 초안을 저장하고 실제 저장 항목·첨부를 다시 읽어 확인한 뒤, 시도 흔적으로 무심코 재실행하는 것을 막는 코드를 다룹니다.

지금 확인한 것은 ZIP 입력 바이트, Python 참조 검사와 VBA 코드 구조입니다. Outlook·Excel을 실행하거나 실제 초안을 만들지 않았습니다. SMTP 전송·외부 서버 접속도 하지 않았으며 성공한 Outlook 영수증을 꾸며 첨부하지 않습니다.

example.com 가상 수신자 두 명의 미발송 초안과 attachment.txt의 60바이트·LF 입력 계약을 설명한 도해
example.com 가상 수신자 두 명의 미발송 초안과 attachment.txt의 60바이트·LF 입력 계약을 설명한 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.

연습 입력은 실제 수신자를 받지 않습니다

practice-68.xlsx의 Recipients에는 아래 두 행만 있습니다. 별도 outlook-draft-demo.xlsm으로 저장하고 attachment.txt와 같은 쓰기 가능한 로컬 연습 폴더에 둡니다. Windows의 클래식 Outlook을 대상으로 한 예제이며 다른 Outlook 제품에서의 COM 호환성은 확인하지 않았습니다.

Email Subject Attachment
sample1@example.com Practice A attachment.txt
sample2@example.com Practice B attachment.txt

코드는 도메인이 정확히 example.com인 제한된 ASCII 주소만 허용합니다. 이는 실습 범위를 좁힌 규칙이지 일반 이메일 주소 검증기나 실제 전달 가능성 검사기가 아닙니다. 같은 수신자·제목 조합의 중복, 제어 문자 제목, 수식 입력 셀, 병합·오류 셀, 불완전한 꼬리 행은 거부합니다. 데이터 행 상한 30은 실습 제한이며 처리 속도 보장이 아닙니다.

첨부는 다음 ASCII 문장 뒤 LF 한 바이트를 붙인 파일입니다. CRLF나 BOM으로 다시 저장하면 같은 글자로 보여도 이 예제의 바이트 계약과 달라집니다.

Synthetic attachment for an Outlook draft. No private data.

파일명은 attachment.txt로 고정하고 절대 경로나 상위 폴더 경로를 받지 않습니다. 개인 파일을 이 이름으로 바꿔 넣어도 내용 바이트가 달라 거부됩니다.

저장 전 기록과 확인 완료는 다른 단계입니다

실행은 입력·첨부 검사와 기본값이 “아니오”인 확인창을 거친 뒤 blogops-outlook-practice-attempt 폴더를 새로 확보합니다. 이 이름의 파일이나 폴더가 이미 있으면 초안을 만들기 전에 중단합니다. 기존 시도 흔적을 자동 삭제하지 않습니다.

새 Outlook 항목을 Save하면 그 항목 유형의 기본 폴더에 저장됩니다. 첨부 컬렉션을 추가·제거하기 전에 항목을 먼저 저장하도록 공식 문서가 안내합니다. 코드도 첫 Save로 식별자를 확보한 뒤 첨부를 추가하고 다시 저장합니다.

시도 기록 의미 아직 확인되지 않은 점
START 로컬 시도 폴더 확보 초안 저장 여부
SAVE_PENDING 곧 Save를 호출함 호출 오류가 나도 저장됐을 가능성
INITIAL_SAVED EntryID·StoreID 확보 첨부와 최종 필드 일치
VERIFIED 재조회한 항목과 첨부 바이트 일치 서버 동기화·메일 전달
COMPLETE verified=2 두 행의 로컬 확인 종료 발송 성공을 뜻하지 않음

GetItemFromID는 유효한 EntryID로 지정한 Outlook 항목을 반환합니다. 재조회에서는 미발송 메일인지, 받는 사람 하나·빈 CC/BCC·제목·본문·첨부 하나가 맞는지 확인합니다. 첨부는 별도의 새로운 readback 파일로 저장해 바이트까지 대조합니다. 같은 이름의 기존 readback 파일을 덮지 않습니다.

입력과 첨부 확인 뒤 시도 폴더를 확보하고 첫 Save·첨부 추가·재저장·ID 재조회·바이트 재검사를 하는 도해
입력과 첨부 확인 뒤 시도 폴더를 확보하고 첫 Save·첨부 추가·재저장·ID 재조회·바이트 재검사를 하는 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.

초안만 다루는 전체 모듈

PracticeModule.bas를 해당 XLSM 표준 모듈로 가져오고 CreateMailDraftsOnly를 사용합니다. 코드는 Send를 호출하지 않습니다. 자동 전달 기능을 추가하는 방향으로 이 실습을 설명하지 않습니다.

첫 Attribute VB_Name 줄은 BAS 가져오기용 메타데이터입니다. BAS를 가져올 때는 그대로 두고, 코드 창에 직접 붙여넣을 때는 이 첫 줄만 제외합니다.

Attribute VB_Name = "PracticeModule"
Option Explicit

Private Const ATTACHMENT_NAME As String = "attachment.txt"
Private Const ATTACHMENT_TEXT As String = "Synthetic attachment for an Outlook draft. No private data."
Private Const ATTEMPT_NAME As String = "blogops-outlook-practice-attempt"
Private Const MAX_ROWS As Long = 30
Private mBusy As Boolean

Public Sub CreateMailDraftsOnly()
    Dim ws As Worksheet, found As Range
    Dim fso As Object, seen As Object, app As Object, session As Object
    Dim mail As Object, check As Object, recipient As Object, attachment As Object
    Dim lastRow As Long, r As Long, i As Long, n As Long, verified As Long
    Dim inputs() As String, key As String, sourcePath As String
    Dim attemptPath As String, receiptPath As String, stagedPath As String
    Dim entryID As String, storeID As String, bodyText As String, readbackPath As String
    Dim phase As String, message As String, failText As String, logError As String
    Dim attemptOwned As Boolean, saveMayHaveHappened As Boolean

    If mBusy Then
        MsgBox "This module is already running.", vbExclamation
        Exit Sub
    End If
    mBusy = True
    On Error GoTo Failed
    phase = "preflight"
    If InStr(1, Application.OperatingSystem, "Mac", vbTextCompare) > 0 Then
        Err.Raise vbObjectError + 680, , "Windows classic Outlook is required."
    End If
    If Len(ThisWorkbook.Path) = 0 Or ThisWorkbook.FileFormat <> 52 Then
        Err.Raise vbObjectError + 681, , "Save a separate XLSM controller first."
    End If
    Set fso = CreateObject("Scripting.FileSystemObject")
    If Not fso.FolderExists(ThisWorkbook.Path) Then
        Err.Raise vbObjectError + 682, , "Use a writable local practice folder."
    End If
    Set ws = ThisWorkbook.Worksheets("Recipients")
    If CellText(ws.Range("A1")) <> "Email" Or _
       CellText(ws.Range("B1")) <> "Subject" Or _
       CellText(ws.Range("C1")) <> "Attachment" Then
        Err.Raise vbObjectError + 683, , "Expected Email, Subject, Attachment headers."
    End If
    Set found = ws.Range("A:C").Find(What:="*", After:=ws.Range("A1"), _
        LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, _
        SearchDirection:=xlPrevious, MatchCase:=False, SearchFormat:=False)
    If found Is Nothing Then Err.Raise vbObjectError + 684, , "Input is empty."
    lastRow = found.Row
    n = lastRow - 1
    If n < 1 Or n > MAX_ROWS Then
        Err.Raise vbObjectError + 685, , "Use 1 to 30 complete synthetic rows."
    End If
    ReDim inputs(1 To n, 1 To 3)
    Set seen = CreateObject("Scripting.Dictionary")
    seen.CompareMode = vbTextCompare
    For r = 2 To lastRow
        i = r - 1
        inputs(i, 1) = Trim$(CellText(ws.Cells(r, 1)))
        inputs(i, 2) = Trim$(CellText(ws.Cells(r, 2)))
        inputs(i, 3) = CellText(ws.Cells(r, 3))
        If Not IsSampleAddress(inputs(i, 1)) Then
            Err.Raise vbObjectError + 686, , "Invalid sample mailbox at row " & CStr(r)
        End If
        If Len(inputs(i, 2)) < 1 Or Len(inputs(i, 2)) > 120 Or _
           HasControlCharacter(inputs(i, 2)) Then
            Err.Raise vbObjectError + 687, , "Invalid subject at row " & CStr(r)
        End If
        If StrComp(inputs(i, 3), ATTACHMENT_NAME, vbBinaryCompare) <> 0 Then
            Err.Raise vbObjectError + 688, , "Only attachment.txt is allowed."
        End If
        key = inputs(i, 1) & vbTab & inputs(i, 2)
        If seen.Exists(key) Then
            Err.Raise vbObjectError + 689, , "Duplicate recipient/subject row."
        End If
        seen.Add key, True
    Next r
    sourcePath = fso.BuildPath(ThisWorkbook.Path, ATTACHMENT_NAME)
    CheckFixtureBytes fso, sourcePath
    attemptPath = fso.BuildPath(ThisWorkbook.Path, ATTEMPT_NAME)
    If fso.FolderExists(attemptPath) Or fso.FileExists(attemptPath) Then
        Err.Raise vbObjectError + 690, , _
            "A previous attempt exists. Inspect its receipt and Outlook drafts before any repeat."
    End If
    If MsgBox("Create " & CStr(n) & " synthetic drafts in the current Outlook profile?" & _
        vbCrLf & "Drafts may sync to that account. Do not press Send." & _
        vbCrLf & "The attempt folder will block blind repeat runs.", _
        vbYesNo + vbQuestion + vbDefaultButton2) <> vbYes Then
        message = "Cancelled before draft creation."
        GoTo Finished
    End If

    phase = "reserve attempt folder"
    fso.CreateFolder attemptPath
    attemptOwned = True
    receiptPath = fso.BuildPath(attemptPath, "receipt.txt")
    WriteReceipt fso, receiptPath, "START " & Format$(Now, "yyyy-mm-dd hh:nn:ss") & _
        " rows=" & CStr(n), True
    stagedPath = fso.BuildPath(attemptPath, ATTACHMENT_NAME)
    WriteFixture fso, stagedPath
    CheckFixtureBytes fso, stagedPath

    phase = "connect to classic Outlook"
    Set app = CreateObject("Outlook.Application")
    Set session = app.GetNamespace("MAPI")
    For i = 1 To n
        r = i + 1
        entryID = ""
        storeID = ""
        phase = "prepare row " & CStr(r)
        bodyText = "Practice draft. Review the recipient and attachment manually." & _
            vbCrLf & "BlogOps practice; worksheet row " & CStr(r) & ". Do not send."
        Set mail = app.CreateItem(0)
        mail.BodyFormat = 1
        Set recipient = mail.Recipients.Add(inputs(i, 1))
        recipient.Type = 1
        mail.Subject = inputs(i, 2)
        mail.Body = bodyText

        phase = "initial save row " & CStr(r)
        WriteReceipt fso, receiptPath, "SAVE_PENDING row=" & CStr(r), False
        saveMayHaveHappened = True
        mail.Save
        entryID = CStr(mail.EntryID)
        storeID = CStr(mail.Parent.StoreID)
        If Len(entryID) = 0 Or Len(storeID) = 0 Then
            Err.Raise vbObjectError + 691, , "Saved item identity is unavailable."
        End If
        WriteReceipt fso, receiptPath, "INITIAL_SAVED row=" & CStr(r) & _
            " EntryID=" & entryID & " StoreID=" & storeID, False

        phase = "attach and save row " & CStr(r)
        CheckFixtureBytes fso, stagedPath
        Set attachment = mail.Attachments.Add(stagedPath, 1)
        mail.Save
        Set attachment = Nothing
        Set recipient = Nothing
        Set mail = Nothing

        phase = "read back row " & CStr(r)
        Set check = session.GetItemFromID(entryID, storeID)
        If check.Class <> 43 Or check.Sent Then
            Err.Raise vbObjectError + 692, , "Saved item is not an unsent mail item."
        End If
        If check.Recipients.Count <> 1 Then
            Err.Raise vbObjectError + 693, , "Expected exactly one recipient."
        End If
        Set recipient = check.Recipients.Item(1)
        If recipient.Type <> 1 Or _
           StrComp(CStr(recipient.Address), inputs(i, 1), vbTextCompare) <> 0 Then
            Err.Raise vbObjectError + 694, , "Recipient readback differs."
        End If
        If Len(CStr(check.CC)) <> 0 Or Len(CStr(check.BCC)) <> 0 Or _
           StrComp(CStr(check.Subject), inputs(i, 2), vbBinaryCompare) <> 0 Then
            Err.Raise vbObjectError + 695, , "Subject or recipient fields differ."
        End If
        If StrComp(NormalizeBody(CStr(check.Body)), NormalizeBody(bodyText), vbBinaryCompare) <> 0 Then
            Err.Raise vbObjectError + 696, , "Body readback differs."
        End If
        If check.Attachments.Count <> 1 Then
            Err.Raise vbObjectError + 697, , "Expected exactly one attachment."
        End If
        Set attachment = check.Attachments.Item(1)
        If StrComp(CStr(attachment.FileName), ATTACHMENT_NAME, vbBinaryCompare) <> 0 Or _
           attachment.Type <> 1 Then
            Err.Raise vbObjectError + 698, , "Attachment identity differs."
        End If
        readbackPath = fso.BuildPath(attemptPath, "readback-row-" & CStr(r) & ".txt")
        If fso.FileExists(readbackPath) Or fso.FolderExists(readbackPath) Then
            Err.Raise vbObjectError + 699, , "Readback path already exists; no overwrite."
        End If
        attachment.SaveAsFile readbackPath
        CheckFixtureBytes fso, readbackPath
        WriteReceipt fso, receiptPath, "VERIFIED row=" & CStr(r) & _
            " EntryID=" & entryID & " StoreID=" & storeID, False
        verified = verified + 1
        Set attachment = Nothing
        Set recipient = Nothing
        Set check = Nothing
    Next i
    WriteReceipt fso, receiptPath, "COMPLETE verified=" & CStr(verified), False
    message = CStr(verified) & " drafts verified and logged." & vbCrLf & _
        "This macro did not call Send. Review Drafts; do not send these examples." & _
        vbCrLf & "Keep the attempt folder: " & attemptPath
    GoTo Finished

Failed:
    failText = Err.Description
    message = "Stopped at " & phase & ": " & failText & vbCrLf & _
        "Verified and logged rows: " & CStr(verified) & "."
    If saveMayHaveHappened Then
        message = message & vbCrLf & "Saved or incomplete drafts may remain. No automatic deletion or retry."
    Else
        message = message & vbCrLf & "This attempt did not reach a draft Save call."
    End If
    If attemptOwned Then
        message = message & vbCrLf & "Keep and inspect: " & attemptPath
        On Error Resume Next
        Err.Clear
        WriteReceipt fso, receiptPath, "STOP phase=" & phase & _
            " verified=" & CStr(verified) & " error=" & failText, False
        If Err.Number <> 0 Then logError = Err.Description
        On Error GoTo 0
        If Len(logError) > 0 Then message = message & vbCrLf & "Receipt update unconfirmed: " & logError
    End If

Finished:
    Set attachment = Nothing
    Set recipient = Nothing
    Set check = Nothing
    Set mail = Nothing
    Set session = Nothing
    Set app = Nothing
    Set seen = Nothing
    Set fso = Nothing
    mBusy = False
    If Len(message) > 0 Then MsgBox message, vbInformation
End Sub

Private Function CellText(ByVal cell As Range) As String
    If cell.HasFormula Or cell.MergeCells Or IsError(cell.Value2) Then
        Err.Raise vbObjectError + 700, , "Formula, merged or error input cell: " & cell.Address
    End If
    If IsEmpty(cell.Value2) Then
        CellText = ""
    ElseIf VarType(cell.Value2) = vbString Then
        CellText = CStr(cell.Value2)
    Else
        Err.Raise vbObjectError + 701, , "Expected literal text: " & cell.Address
    End If
End Function

Private Function HasControlCharacter(ByVal value As String) As Boolean
    Dim i As Long, code As Long
    For i = 1 To Len(value)
        code = AscW(Mid$(value, i, 1))
        If code >= 0 And (code < 32 Or code = 127) Then
            HasControlCharacter = True
            Exit Function
        End If
    Next i
End Function

Private Function IsSampleAddress(ByVal value As String) As Boolean
    Dim parts As Variant, localPart As String, i As Long, code As Long, ch As String
    parts = Split(value, "@")
    If UBound(parts) <> 1 Then Exit Function
    If StrComp(CStr(parts(1)), "example.com", vbTextCompare) <> 0 Then Exit Function
    localPart = CStr(parts(0))
    If Len(localPart) < 1 Or Len(localPart) > 64 Then Exit Function
    If Left$(localPart, 1) = "." Or Right$(localPart, 1) = "." Or _
       InStr(localPart, "..") > 0 Then Exit Function
    For i = 1 To Len(localPart)
        ch = Mid$(localPart, i, 1)
        code = AscW(ch)
        If Not ((code >= 48 And code <= 57) Or (code >= 65 And code <= 90) Or _
                (code >= 97 And code <= 122) Or InStr(1, "._+-", ch, vbBinaryCompare) > 0) Then
            Exit Function
        End If
    Next i
    IsSampleAddress = True
End Function

Private Function NormalizeBody(ByVal value As String) As String
    value = Replace(value, vbCrLf, vbLf)
    value = Replace(value, vbCr, vbLf)
    Do While Len(value) > 0
        If Right$(value, 1) <> vbLf Then Exit Do
        value = Left$(value, Len(value) - 1)
    Loop
    NormalizeBody = value
End Function

Private Sub CheckFixtureBytes(ByVal fso As Object, ByVal filePath As String)
    Dim handle As Integer, opened As Boolean, data() As Byte, expected As String
    Dim i As Long, number As Long, description As String
    On Error GoTo Failed
    expected = ATTACHMENT_TEXT & vbLf
    If Not fso.FileExists(filePath) Then
        Err.Raise vbObjectError + 702, , "Missing synthetic attachment."
    End If
    handle = FreeFile
    Open filePath For Binary Access Read Lock Write As #handle
    opened = True
    If LOF(handle) <> Len(expected) Then
        Err.Raise vbObjectError + 703, , "Synthetic attachment byte length differs."
    End If
    ReDim data(0 To Len(expected) - 1)
    Get #handle, , data
    For i = 0 To UBound(data)
        If data(i) <> AscW(Mid$(expected, i + 1, 1)) Then
            Err.Raise vbObjectError + 704, , "Synthetic attachment bytes differ."
        End If
    Next i
    Close #handle
    opened = False
    Exit Sub
Failed:
    number = Err.Number
    description = Err.Description
    On Error Resume Next
    If opened Then Close #handle
    On Error GoTo 0
    Err.Raise number, "CheckFixtureBytes", description
End Sub

Private Sub WriteFixture(ByVal fso As Object, ByVal filePath As String)
    Dim stream As Object, number As Long, description As String
    On Error GoTo Failed
    Set stream = fso.CreateTextFile(filePath, False, False)
    stream.Write ATTACHMENT_TEXT & vbLf
    stream.Close
    Set stream = Nothing
    Exit Sub
Failed:
    number = Err.Number
    description = Err.Description
    On Error Resume Next
    If Not stream Is Nothing Then stream.Close
    On Error GoTo 0
    Err.Raise number, "WriteFixture", description
End Sub

Private Sub WriteReceipt(ByVal fso As Object, ByVal filePath As String, _
                         ByVal value As String, ByVal first As Boolean)
    Dim stream As Object, number As Long, description As String
    On Error GoTo Failed
    If first Then
        Set stream = fso.CreateTextFile(filePath, False, True)
    Else
        Set stream = fso.OpenTextFile(filePath, 8, False, -1)
    End If
    stream.WriteLine value
    stream.Close
    Set stream = Nothing
    Exit Sub
Failed:
    number = Err.Number
    description = Err.Description
    On Error Resume Next
    If Not stream Is Nothing Then stream.Close
    On Error GoTo 0
    Err.Raise number, "WriteReceipt", description
End Sub

초안을 확인할 Windows 실습 순서

  1. Windows 클래식 Outlook을 사용하는 별도 연습 환경이 필요합니다. 실제 실행은 아직 검증하지 않았습니다.

  2. practice-68.xlsx를 outlook-draft-demo.xlsm으로 저장하고 attachment.txt 원본 바이트를 같은 폴더에 둡니다.

  3. PracticeModule.bas를 가져옵니다. 실제 주소·개인 첨부로 바꾸지 않습니다.

  4. CreateMailDraftsOnly는 초안 저장 전 기본 아니오 확인창을 표시합니다. Send 호출은 없습니다.

  5. 실제 실행 시 sample1@example.com/Practice A와 sample2@example.com/Practice B의 미발송 초안·첨부·receipt.txt를 대조합니다.

  6. 부분 실패 또는 기존 시도 폴더가 있으면 자동 삭제·재시도하지 않습니다. Python verification.json은 실제 Outlook 영수증이 아닙니다.

검증 결과와 사용할 환경에서의 확인

Python 검사는 실제 Recipients의 두 행과 attachment.txt의 정확한 바이트를 읽고, 허용 주소·중복·잘못된 경로·첨부 변형을 검사했습니다. 임시 폴더에서는 이미 확보한 시도 경로를 두 번째 실행이 덮지 못하는 규칙을 검산합니다. 저장 중단 모델에서 SAVE_PENDING을 VERIFIED로 해석하지 않는 것도 확인합니다. 이 기록들은 참조 테스트이며 Outlook에서 생성된 receipt.txt가 아닙니다.

독자가 Windows 연습 환경에서 실행한다면 먼저 두 초안만 예상된다는 것을 확인합니다. 완료 후 초안함에서 sample1@example.com/Practice A와 sample2@example.com/Practice B를 각각 열어 미발송 상태와 첨부를 확인하고, 시도 폴더의 실제 receipt.txt와 대조합니다. 수신·서버 동기화 여부는 이 코드의 완료 조건에 포함되지 않습니다.

START·SAVE_PENDING·INITIAL_SAVED·VERIFIED·COMPLETE의 차이와 확인0건이어도 초안이 남을 수 있음을 설명한 도해
START·SAVE_PENDING·INITIAL_SAVED·VERIFIED·COMPLETE의 차이와 확인0건이어도 초안이 남을 수 있음을 설명한 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.

중간 실패는 흔적을 남긴 채 멈춥니다

Outlook을 열기 전 첨부 바이트 불일치로 중단됐다면, ZIP에서 attachment.txt를 새 연습 폴더로 다시 복사해 입력을 복구합니다. 텍스트 편집기의 개행 변환·BOM 추가를 제거하려고 파일을 재편집하기보다 제공된 정확한 바이트와 비교하세요. 이미 시도 폴더가 만들어진 실패라면 이 입력 복구만으로 재실행 자격이 생기지는 않습니다.

첫 Save 직전부터는 오류가 났어도 초안이 남았을 수 있습니다. 검증 완료 수가 0이라는 이유로 아무것도 저장되지 않았다고 판단하면 안 됩니다. STOP 기록의 단계, INITIAL_SAVED의 식별자, 실제 초안함을 함께 확인합니다. 코드가 생성 초안을 자동 삭제하거나 재시도하지 않는 이유입니다.

재실행 차단 메시지가 나오면 시도 폴더를 지워 우회하지 않습니다. 이전 결과가 확인되지 않은 상태에서 새 시도 폴더 이름만 만드는 것도 같은 중복 위험을 숨깁니다. 연습을 다시 하려면 이전 흔적과 초안을 먼저 사람이 대조하고 별도 실습 건으로 정리해야 합니다.

적용 한계

이 코드는 메일 전달, 서버 측 중복 제거, Outlook 강제 종료 뒤 완전 복구를 보장하지 않습니다. 시도 경로는 로컬 보수적 차단 장치입니다. 실제 COM 호출, 프로필·보안 대화상자, StoreID 재조회와 첨부 저장은 아직 실행 검증하지 않았습니다. 업무 주소·개인 첨부로 바꾸기 전에 별도 요구와 테스트가 필요합니다.

공식 자료

MailItem.Save method (Outlook)

Attachments.Add method (Outlook)

NameSpace.GetItemFromID method (Outlook)

출처 원문 수집일: S1 2026-09-24; S2 2026-09-24; S3 2026-09-24.

작성자 소개