더북(TheBook)

이메일 전송

UTL_SMTP와 UTL_MAIL을 이용해 이메일을 보내는 방법을 배웠는데 이전 장에서는 익명 블록으로 메일을 보냈지만, 이번에는 보내는 사람, 받는 사람 주소, 제목, 메일 내용, 첨부파일 등을 매개변수로 입력받아 이메일을 보내는 email_send_prc 라는 프로시저를 만들어 보자. 지금까지 만든 다른 유틸리티 프로그램과는 달리 이 프로시저에서는 패키지 변수를 선언해 사용할 것이다. SMTP 서버 주소명, 포트번호, 도메인, boundary 문자열이 그 대상이다.

먼저 패키지 선언부에서 관련 프로시저와 패키지 상수를 선언해 보자.

입력

    CREATE OR REPLACE PACKAGE BODY my_util_pkg IS
    ...
    ...
      -- 이메일 전송과 관련된 패키지 상수
      pv_host   VARCHAR2(10)  := 'localhost';  -- SMTP 서버명
      pn_port   NUMBER        := 25;           -- 포트번호
      pv_domain VARCHAR2(30) := 'hong.com';    -- 도메인명

      pv_boundary VARCHAR2(50) := 'DIFOJSLKDWFEFO.WEFOWJFOWE';  -- boundary text
      pv_directory VARCHAR2(50) := 'SMTP_FILE'; --파일이 있는 디렉토리명

      -- 5. 이메일 전송
      PROCEDURE email_send_prc ( ps_from    IN VARCHAR2,
                                 ps_to      IN VARCHAR2,
                                 ps_subject IN VARCHAR2,
                                 ps_body    IN VARCHAR2,
                                 ps_content IN VARCHAR2  DEFAULT 'text/plain;',
                                 ps_file_nm IN VARCHAR2
                               );

    END my_util_pkg;

결과

    PACKAGE MY_UTIL_PKG이(가) 컴파일되었습니다.

이제 패키지 본문, 즉 email_send_prc 프로시저의 본문을 작성해야 하는데 기본적인 내용은 이전 장에서 살펴봤던 예제와 크게 다르지 않은데 이 익명 블록 예제는 한 가지 문제점이 있다. 바로 크기가 32kb 이하의 파일만 첨부해 보낼 수 있다는 점이다.

이전 장의 익명 블록을 사용해 32KB 이상 파일을 첨부하면 오류가 발생하는데, 그 원인은 UTL_SMTP 패키지의 문제가 아니라 바로 파일 이름을 입력받아 해당 파일을 RAW 타입으로 반환하는 fn_get_raw_file 함수의 문제다. 이 함수의 소스를 보면 내부적으로 UTL_FILE.GET_RAW 프로시저를 사용해서 해당 파일을 vf_raw라는 변수에 넣어 이를 반환하는데, vf_raw는 RAW 타입으로 최댓값이 32767, 즉 32KB이다. 따라서 32KB 이상의 파일을 읽어 반환할 수 없는 것이다.

따라서 32KB보다 크기가 큰 파일을 처리하려면 RAW 타입이 아닌 LOB 타입을 사용해야 한다. email_send_prc 프로시저에서는 파일 처리를 LOB 타입 중 하나인 BFILE 타입으로 받아 처리할 것이다. 그럼 프로시저를 작성해 보자.

입력

    CREATE OR REPLACE PACKAGE BODY my_util_pkg IS
    ...
    ...
      PROCEDURE email_send_prc ( ps_from    IN VARCHAR2,  -- 보내는 사람
                                 ps_to      IN VARCHAR2,  -- 받는 사람
                                 ps_subject IN VARCHAR2,  -- 제목
                                 ps_body    IN VARCHAR2,  -- 본문
                                 ps_content IN VARCHAR2  DEFAULT 'text/plain;', 
                                 ps_file_nm IN VARCHAR2  -- 첨부파일
                               )
      IS
        vc_con utl_smtp.connection;

        v_bfile        BFILE;       -- 파일을 담을 변수
        vn_bfile_size  NUMBER := 0; -- 파일크기

        v_temp_blob    BLOB := EMPTY_BLOB; -- 파일을 옮겨담을 BLOB 타입 변수
        vn_blob_size   NUMBER := 0;        -- BLOB 변수 크기
        vn_amount      NUMBER := 54;       -- 54 단위로 파일을 잘라 메일에 붙이기 위함
        v_tmp_raw      RAW(54);            -- 54 단위로 자른 파일내용이 담긴 RAW 타입변수
        vn_pos         NUMBER := 1;      --파일 위치를 담는 변수
      BEGIN

        vc_con := UTL_SMTP.OPEN_CONNECTION(pv_host, pn_port);
        UTL_SMTP.HELO(vc_con, pv_domain); -- HELO
        UTL_SMTP.MAIL(vc_con, ps_from);   -- 보내는사람
        UTL_SMTP.RCPT(vc_con, ps_to);     -- 받는사람

        UTL_SMTP.OPEN_DATA(vc_con); -- 메일본문 작성 시작
        UTL_SMTP.WRITE_DATA(vc_con,'MIME-Version: 1.0' || UTL_TCP.CRLF ); -- MIME 버전

        UTL_SMTP.WRITE_DATA(vc_con,'Content-Type: multipart/mixed; boundary="' || pv_boundary || '"' || UTL_TCP.CRLF);
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('From: ' || ps_from || UTL_TCP.CRLF) );
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('To: ' || ps_to || UTL_TCP.CRLF) );
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('Subject: ' || ps_subject || UTL_TCP.CRLF) );
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );

        -- 메일 본문
        UTL_SMTP.WRITE_DATA(vc_con, '--' || pv_boundary || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, 'Content-Type: ' || ps_content || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, 'charset=euc-kr' || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW(ps_body || UTL_TCP.CRLF)  );
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );

        -- 첨부파일이 있다면 ...
        IF ps_file_nm IS NOT NULL THEN

          UTL_SMTP.WRITE_DATA(vc_con, '--' || pv_boundary || UTL_TCP.CRLF );
          -- 파일의 Content-Type은 application/octet-stream
          UTL_SMTP.WRITE_DATA(vc_con,'Content-Type: application/octet-stream; name="' || ps_file_nm || '"' || UTL_TCP.CRLF);
          UTL_SMTP.WRITE_DATA(vc_con,'Content-Transfer-Encoding: base64' || UTL_TCP.CRLF);
          UTL_SMTP.WRITE_DATA(vc_con,'Content-Disposition: attachment; filename="' || ps_file_nm || '"' || UTL_TCP.CRLF);

          UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF);

          -- 파일처리 시작
          -- 파일을 읽어 BFILE 변수인 v_bfile에 담는다.
          v_bfile := BFILENAME(pv_directory, ps_file_nm);
          -- v_bfile 담은 파일을 읽기전용으로 연다.
          DBMS_LOB.OPEN(v_bfile, DBMS_LOB.LOB_READONLY);
          -- v_bfile에 담긴 파일의 크기를 가져온다.
          vn_bfile_size := DBMS_LOB.GETLENGTH(v_bfile);

          -- v_bfile를 BLOB 변수인 v_temp_blob에 담기 위해 초기화
          DBMS_LOB.CREATETEMPORARY(v_temp_blob, TRUE);
          -- v_bfile에 담긴 파일을 v_temp_blob 로 옮긴다.
          DBMS_LOB.LOADFROMFILE(v_temp_blob, v_bfile, vn_bfile_size);
          -- v_temp_blob의 크기를 구한다.
          vn_blob_size := DBMS_LOB.GETLENGTH(v_temp_blob);

          -- vn_pos 초기값은 1, v_temp_blob 크기보다 작은 경우 루프
          WHILE vn_pos < vn_blob_size
          LOOP
              -- v_temp_blob에 담긴 파일을 vn_amount(54)씩 잘라  v_tmp_raw에 담는다.
              DBMS_LOB.READ(v_temp_blob, vn_amount, vn_pos, v_tmp_raw);
              -- 잘라낸 v_tmp_raw를 메일에 첨부한다.
              UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_ENCODE.BASE64_ENCODE ( v_tmp_raw));
              UTL_SMTP.WRITE_DATA(vc_con,  UTL_TCP.CRLF );

              v_tmp_raw := NULL;
              vn_pos := vn_pos + vn_amount;
            END LOOP;

            DBMS_LOB.FREETEMPORARY(v_temp_blob); -- v_temp_blob 메모리 해제
            DBMS_LOB.FILECLOSE(v_bfile); -- v_bfile 닫기

        END IF; -- 첨부파일 처리 종료

        -- 맨 마지막 boundary에는 앞과 뒤에 '--'를 반드시 붙여야 한다.
        UTL_SMTP.WRITE_DATA(vc_con, '--' ||  pv_boundary || '--' || UTL_TCP.CRLF );

        UTL_SMTP.CLOSE_DATA(vc_con); -- 메일 본문 작성 종료
        UTL_SMTP.QUIT(vc_con);       -- 메일 세션 종료


      EXCEPTION
        WHEN UTL_SMTP.INVALID_OPERATION THEN
        dbms_output.put_line(' Invalid Operation in Mail attempt using UTL_SMTP.');
        dbms_output.put_line(sqlerrm);
        UTL_SMTP.QUIT(vc_con);
      WHEN UTL_SMTP.TRANSIENT_ERROR THEN
        dbms_output.put_line(' Temporary e-mail issue - try again');
        UTL_SMTP.QUIT(vc_con);
      WHEN UTL_SMTP.PERMANENT_ERROR THEN
        dbms_output.put_line(' Permanent Error Encountered.');
        dbms_output.put_line(sqlerrm);
        UTL_SMTP.QUIT(vc_con);
      WHEN OTHERS THEN
        dbms_output.put_line(sqlerrm);
        UTL_SMTP.QUIT(vc_con);

      END email_send_prc;
    ...
    ...

결과

    PACKAGE BODY MY_UTIL_PKG이(가) 컴파일되었습니다.

이제 HTML 형태의 첨부파일이 포함된 메일을 보내 보자. 이번에 첨부할 파일은 그 크기가 32kb 이상인 “seoul.xls”파일을 전송해 볼 텐데 그 전에 해당 파일을 “C:\ch18_file” 디렉토리에 넣은 다음 프로시저를 호출해 보자.

입력

    DECLARE
      vv_html VARCHAR2(1000);
    BEGIN
      vv_html := '<HTML> <HEAD>
       <TITLE>HTML 테스트</TITLE>
     </HEAD>
     <BDOY>
        <p>이 메일은 <b>HTML</b> <i>버전</i> 으로 </p>
        <p> <strong>my_util_pkg</strong> 패키지의 email_send_prc 프로시저를 사용해 보낸 메일입니다. </p>
     </BODY>
    </HTML>';

      -- 이메일전송
      my_util_pkg.email_send_prc ( ps_from => 'charieh@hong.com'
                                   ,ps_to => 'charieh@hong.com'
                                   ,ps_subject =>  '테스트 메일'
                                   ,ps_body => vv_html
                                   ,ps_content => 'text/html;'
                                   ,ps_file_nm => 'hong1.txt'
                                   );
    END;

결과

    익명 블록이 완료되었습니다.

제대로 전송됐는지 확인해 보자.

그림 19-2 my_util_pkg.email_send_prc로 보낸 이메일

첨부파일을 열어 보자.

그림 19-3 32kb 이상의 첨부파일이 전송된 화면

첨부파일까지 제대로 전송된 것을 알 수 있다. 이 프로시저의 소스가 약간 복잡하게 보이지만, 첨부파일을 보내는 루틴을 별도의 프로시저로 분리해 낸다면 훨씬 가독성이 높아질 것이다.

신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.