2023년 4월 10일 월요일

자바 어플리케이션 원격 모니터링

[ 출처 : http://freegram.egloos.com/ ]

Jconsole, MC4J 를 사용한 자바 어플리케이션 원격 모니터링(JMX)

 

JMX(JavaManagement Extensions)는 실행중인 자바 어플리케이션을 외부에서 모니터링하고 관리하는 기능을 제공한다. 실제로 이 API는 웹서버에서 네트워크 디바이스, 웹폰에 이르기까지 자바로 이용 가능한 것은 어느 것이든 로컬 혹은 원격으로 처리 할수 있게 한다. JMX 기술은 JCP(Java Community Process)에 의해 개발된 밀접한 관계의 두 스펙, Java Specification Request (JSR) 3: Java Management Extensions (JMX) SpecificationJSR 160: Java Management Extensions (JMX) Remote API 1.0에 의해 정의된다.

J2SE platform 5.0 부터 JMX기술은 Java SE 플랫폼의 하나로 통합되었다. 이전 버전에서 JMX 기술의 구현은 해당문서를 참조하도록 한다.

 

이러한 JMX 프로토콜을 사용하여 실행중인 어플리케이션의 메모리 및 스레드등에 대한 그래피컬한 정보를 보여주는 툴로는 JDK에 기본 내장된 JConsole 과 오픈소스 프로젝트에 하나인 MC4J 가 있다.

MC4J는 여러개의 서버 및 JVM 리소스들을 동시에 관리할 수 있는 편리함이 있지만 JConsole 도 버전업이 되면서 그에 못지않게 편리해졌다. 개인적으로 일반적인 용도로 사용할때는 JConsole이 좀 더 직관적이고 편리한 것 같다.

 

Tomcat 5.5 버전 이후부터 JDK 1.5에서 기본지원하는 JMX 프로토콜을 사용함으로 추가적인 연결아답터를 설치할필요가 없다. 이전버전의 JMX 연결설정은 이 문서에서 설명하지 않는다.(일반적으로 5.5 이전 버전에서는 MX4J JMXConnector 를 사용하였다)

 

JDK 설정

원격 모니터링을 활성화하기 위해서 JDK 의 JMX 관련설정을 해주어야 한다.

1) JDK5 이후버전만 가능

2) "JAVA_HOME/jre/lib/management/" 폴더에 있는 "jmxremote.password.template" 파일을 복사해서 "jmxremote.password" 파일을 만든다.

3) "jmxremote.password" 파일을 열고 밑의 계정들에 달린 주석(#)을 제거한다.

monitorRole  QED

controlRole   R&D

4) "jmxremote.password" 파일을 읽기전용으로 만든다.

chmod 400 jmxremote.password

선택)"management.properties" 파일에 아래설정을 추가하면 해당 JDK로 실행되는 모든 프로그램에 해당 JMX 옵션이 디폴트로 일괄적용된다.

com.sun.management.jmxremote
com.sun.management.jmxremote.port=8999
com.sun.management.jmxremote.ssl=false

 

JAVA 프로그램 실행(톰캣 등)

자바 어플리케이션을 실행할때 해당 옵션을 추가한다.

java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.ssl=false ...

 

톰캣의 경우도 마찬가지로 JAVA_OPTS 환경변수에 위의 옵션을 추가하여 실행시 반영되도록 설정한다.

예-윈도우) 톰캣 bin폴더에 run.bat 파일을 생성하고 아래의 내용을 추가한다.

setJAVA_OPTS=%JAVA_OPTS% -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.ssl=false

startup.bat

예-리눅스) 톰캣 bin폴더에 run.sh 파일을 생성하고 아래의 내용을 추가한다.

JAVA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.ssl=false"

export JAVA_OPTS

./startup.sh

 

관련옵션

-Dcom.sun.management.jmxremote.port=8999 : 8999는 포트번호로 시스템에서 사용하지 않는 포트를 임의로 지정하면 된다.

-Dcom.sun.management.jmxremote.ssl=false : ssl를 비활성화 한다. ssl을 활성화하여 보안연결을 사용하고자 한다면 몇가지 추가적인 설정들을 필요로 한다. 이 문서에서는 설명하지 않는다.

-Dcom.sun.management.jmxremote.authenticate=false : JMX연결과정에서 인증을 요청하지 않는다.(누구나 접근가능)

-Dcom.sun.management.jmxremote.password.file=파일경로: JMX 인증관련 설정파일을 따로 지정한다. 유닉스 등 멀티유저 환경에서 자바 어플리케이션을 실행할때 패스워드파일은관리자계정에게만 읽기전용의 속성을 가지고 있으므로 다른 사용자들은 권한문제로 어플리케이션 실행이 실패한다. 따라서 각 사용자마다자신만의 jmxremote.password 파일을 생성해서 명시적으로 사용할 수 있도록 지원한다.

 

프로그램을 실행한 뒤 JMX 포트가 정상적으로 열려있는지 확인하기 위해 아래 명령을 사용한다.

netstat -anp | grep LISTEN

 

원격 모니터링 접속

JConsole 의 경우 간단하게 아이피:포트 로 접속하면 된다. jmxremote.password 파일을 수정하지 않았다면 monitorRole:QED, controlRole:R&D 두가지 계정으로 접근할 수 있을것이다.


MC4J 는 연결을 생성할때 JDK5를 선택하고 localhost:포트번호 를 상황에 맞게 수정해서 접속하면 된다.

톰캣서버가 클러스터링 되어있는 경우라면 연결의 속성을 TOMCAT5.5+ 로 선택하여 접속하면 클러스터링 관련의 특화된 관리기능이 활성화된다.


톰캣 이외에 웹로직, 웹스피어, 제우스등의 WAS들도 기본적으로 JDK5 이상버전에서 작동중이라면 위에서 설명한 JMX옵션을 통해 외부 모니터링이 가능하고 MC4J에서는 연결종류에 따라 특화된 관리기능을 제공한다.

 

리눅스에서 JMX 버그

이 글을 포스팅하게 만든 주 원인으로 리눅스상에서 JMX 포트가 열려있더라도 원격에서 접속이 실패하는 현상이 발생한다.리눅스의 호스트테이블을 JDK에서 잘못 인식하여 발생하는 버그로 추정된다. 해결방법은 /etc/hosts 파일을 편집하여

127.0.0.1      localhost.localdomain localhost

실제아이피   호스트명

형식으로 수정한다. 내 경우

127.0.0.1        smbServer localhost.localdomain localhost

::1                 smbServer

로 되어있던 것을 아래와 같이 수정해주었다. 127.0.0.1에 중복정의되어 있던 호스트명(smbServer)를 실제아이피에만 사상한것을 볼 수 있다.

127.0.0.1        localhost.localdomain localhost

192.168.1.40   smbServer


유동아이피라면 굳이 ::1 을 실제아이피로 수정하지 않더라도 localhost와 호스트명(smbServer)만 분리하여 사상하면 정상작동 하리라 본다(미확인)

 

또한 리눅스 방화벽이 JMX 포트에 대해 열려있는지도 확인해야 한다.(etc/rc.d/init.d/iptables --list) 역시나 본 문서에서는 설명하지 않는다.

 

2023년 3월 28일 화요일

[FORMS] Email From Oracle PL/SQL (UTL_SMTP)

 [출처 : https://oracle-base.com/]

The UTL_SMTP package was introduced in Oracle 8i and can be used to send emails from PL/SQL.

Simple Emails

In it's simplest form a single string or variable can be sent as the message body using the following procedure. In this case we have not included any header information or subject line in the message, so it is not very useful, but it is small.

CREATE OR REPLACE PROCEDURE send_mail (p_to        IN VARCHAR2,
                                       p_from      IN VARCHAR2,
                                       p_message   IN VARCHAR2,
                                       p_smtp_host IN VARCHAR2,
                                       p_smtp_port IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  UTL_SMTP.rcpt(l_mail_conn, p_to);
  UTL_SMTP.data(l_mail_conn, p_message || UTL_TCP.crlf || UTL_TCP.crlf);
  UTL_SMTP.quit(l_mail_conn);
END;
/

The code below shows how the procedure is called.

BEGIN
  send_mail(p_to        => 'me@mycompany.com',
            p_from      => 'admin@mycompany.com',
            p_message   => 'This is a test message.',
            p_smtp_host => 'smtp.mycompany.com');
END;
/

Multi-Line Emails

Multi-line messages can be written by expanding the UTL_SMTP.DATA command using the UTL_SMTP.WRITE_DATA command as follows. This is a better method to use as the total message size is no longer constrained by the 32K limit on a VARCHAR2 variable. In the following example the header information has been included in the message also.

CREATE OR REPLACE PROCEDURE send_mail (p_to        IN VARCHAR2,
                                       p_from      IN VARCHAR2,
                                       p_subject   IN VARCHAR2,
                                       p_message   IN VARCHAR2,
                                       p_smtp_host IN VARCHAR2,
                                       p_smtp_port IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  UTL_SMTP.rcpt(l_mail_conn, p_to);

  UTL_SMTP.open_data(l_mail_conn);
  
  UTL_SMTP.write_data(l_mail_conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'To: ' || p_to || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'From: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Subject: ' || p_subject || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Reply-To: ' || p_from || UTL_TCP.crlf || UTL_TCP.crlf);
  
  UTL_SMTP.write_data(l_mail_conn, p_message || UTL_TCP.crlf || UTL_TCP.crlf);
  UTL_SMTP.close_data(l_mail_conn);

  UTL_SMTP.quit(l_mail_conn);
END;
/

The code below shows how the procedure is called.

BEGIN
  send_mail(p_to        => 'me@mycompany.com',
            p_from      => 'admin@mycompany.com',
            p_subject   => 'Test Message',
            p_message   => 'This is a test message.',
            p_smtp_host => 'smtp.mycompany.com');
END;
/

HTML Emails

The following procedure builds on the previous version, allowing it include plain text and/or HTML versions of the email. The format of the message is explained here.

CREATE OR REPLACE PROCEDURE send_mail (p_to        IN VARCHAR2,
                                       p_from      IN VARCHAR2,
                                       p_subject   IN VARCHAR2,
                                       p_text_msg  IN VARCHAR2 DEFAULT NULL,
                                       p_html_msg  IN VARCHAR2 DEFAULT NULL,
                                       p_smtp_host IN VARCHAR2,
                                       p_smtp_port IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
  l_boundary    VARCHAR2(50) := '----=*#abc1234321cba#*=';
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  UTL_SMTP.rcpt(l_mail_conn, p_to);

  UTL_SMTP.open_data(l_mail_conn);
  
  UTL_SMTP.write_data(l_mail_conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'To: ' || p_to || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'From: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Subject: ' || p_subject || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Reply-To: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'MIME-Version: 1.0' || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Content-Type: multipart/alternative; boundary="' || l_boundary || '"' || UTL_TCP.crlf || UTL_TCP.crlf);
  
  IF p_text_msg IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: text/plain; charset="iso-8859-1"' || UTL_TCP.crlf || UTL_TCP.crlf);

    UTL_SMTP.write_data(l_mail_conn, p_text_msg);
    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf || UTL_TCP.crlf);
  END IF;

  IF p_html_msg IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: text/html; charset="iso-8859-1"' || UTL_TCP.crlf || UTL_TCP.crlf);

    UTL_SMTP.write_data(l_mail_conn, p_html_msg);
    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf || UTL_TCP.crlf);
  END IF;

  UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || '--' || UTL_TCP.crlf);
  UTL_SMTP.close_data(l_mail_conn);

  UTL_SMTP.quit(l_mail_conn);
END;
/

The code below shows how the procedure is called.

DECLARE
  l_html VARCHAR2(32767);
BEGIN
  l_html := '<html>
    <head>
      <title>Test HTML message</title>
    </head>
    <body>
      <p>This is a <b>HTML</b> <i>version</i> of the test message.</p>
      <p><img src="http://oracle-base.com/images/site_logo.gif" alt="Site Logo" />
    </body>
  </html>';

  send_mail(p_to        => 'me@mycompany.com',
            p_from      => 'admin@mycompany.com',
            p_subject   => 'Test Message',
            p_text_msg  => 'This is a test message.',
            p_html_msg  => l_html,
            p_smtp_host => 'smtp.mycompany.com');
END;
/

Emails with Attachments

Sending an email with an attachment is similar to the previous example as the message and the attachment must be separated by a boundary and identified by a name and mime type.

BLOB Attachment

Attaching a BLOB requires the binary data to be encoded and converted to text so it can be sent using SMTP.

CREATE OR REPLACE PROCEDURE send_mail (p_to          IN VARCHAR2,
                                       p_from        IN VARCHAR2,
                                       p_subject     IN VARCHAR2,
                                       p_text_msg    IN VARCHAR2 DEFAULT NULL,
                                       p_attach_name IN VARCHAR2 DEFAULT NULL,
                                       p_attach_mime IN VARCHAR2 DEFAULT NULL,
                                       p_attach_blob IN BLOB DEFAULT NULL,
                                       p_smtp_host   IN VARCHAR2,
                                       p_smtp_port   IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
  l_boundary    VARCHAR2(50) := '----=*#abc1234321cba#*=';
  l_step        PLS_INTEGER  := 57;
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  UTL_SMTP.rcpt(l_mail_conn, p_to);

  UTL_SMTP.open_data(l_mail_conn);
  
  UTL_SMTP.write_data(l_mail_conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'To: ' || p_to || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'From: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Subject: ' || p_subject || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Reply-To: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'MIME-Version: 1.0' || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Content-Type: multipart/mixed; boundary="' || l_boundary || '"' || UTL_TCP.crlf || UTL_TCP.crlf);
  
  IF p_text_msg IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: text/plain; charset="iso-8859-1"' || UTL_TCP.crlf || UTL_TCP.crlf);

    UTL_SMTP.write_data(l_mail_conn, p_text_msg);
    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf || UTL_TCP.crlf);
  END IF;

  IF p_attach_name IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: ' || p_attach_mime || '; name="' || p_attach_name || '"' || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Transfer-Encoding: base64' || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Disposition: attachment; filename="' || p_attach_name || '"' || UTL_TCP.crlf || UTL_TCP.crlf);

    FOR i IN 0 .. TRUNC((DBMS_LOB.getlength(p_attach_blob) - 1 )/l_step) LOOP
      UTL_SMTP.write_data(l_mail_conn, UTL_RAW.cast_to_varchar2(UTL_ENCODE.base64_encode(DBMS_LOB.substr(p_attach_blob, l_step, i * l_step + 1))) || UTL_TCP.crlf);
    END LOOP;

    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf);
  END IF;
  
  UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || '--' || UTL_TCP.crlf);
  UTL_SMTP.close_data(l_mail_conn);

  UTL_SMTP.quit(l_mail_conn);
END;
/

Thanks to Sasha in the comments for pointing out the problem with the step size and the need for the extra CRLFs in the loop to prevent a long attachment breaking RFC 2045.

The code below shows how the procedure is called.

DECLARE
  l_name images.name%TYPE := 'site_logo.gif';
  l_blob images.image%TYPE;
BEGIN
  SELECT image
  INTO   l_blob
  FROM   images
  WHERE  name = l_name;
  
  send_mail(p_to          => 'me@mycompany.com',
            p_from        => 'admin@mycompany.com',
            p_subject     => 'Test Message',
            p_text_msg    => 'This is a test message.',
            p_attach_name => 'site_logo.gif',
            p_attach_mime => 'image/gif',
            p_attach_blob => l_blob,
            p_smtp_host   => 'smtp.mycompany.com');
END;
/

CLOB Attachment

Attaching a CLOB is similar to attaching a BLOB, but we don't have to worry about encoding the data because it is already plain text.

CREATE OR REPLACE PROCEDURE send_mail (p_to          IN VARCHAR2,
                                       p_from        IN VARCHAR2,
                                       p_subject     IN VARCHAR2,
                                       p_text_msg    IN VARCHAR2 DEFAULT NULL,
                                       p_attach_name IN VARCHAR2 DEFAULT NULL,
                                       p_attach_mime IN VARCHAR2 DEFAULT NULL,
                                       p_attach_clob IN CLOB DEFAULT NULL,
                                       p_smtp_host   IN VARCHAR2,
                                       p_smtp_port   IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
  l_boundary    VARCHAR2(50) := '----=*#abc1234321cba#*=';
  l_step        PLS_INTEGER  := 12000; -- make sure you set a multiple of 3 not higher than 24573
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  UTL_SMTP.rcpt(l_mail_conn, p_to);

  UTL_SMTP.open_data(l_mail_conn);
  
  UTL_SMTP.write_data(l_mail_conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'To: ' || p_to || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'From: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Subject: ' || p_subject || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Reply-To: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'MIME-Version: 1.0' || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Content-Type: multipart/mixed; boundary="' || l_boundary || '"' || UTL_TCP.crlf || UTL_TCP.crlf);
  
  IF p_text_msg IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: text/plain; charset="iso-8859-1"' || UTL_TCP.crlf || UTL_TCP.crlf);

    UTL_SMTP.write_data(l_mail_conn, p_text_msg);
    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf || UTL_TCP.crlf);
  END IF;

  IF p_attach_name IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Type: ' || p_attach_mime || '; name="' || p_attach_name || '"' || UTL_TCP.crlf);
    UTL_SMTP.write_data(l_mail_conn, 'Content-Disposition: attachment; filename="' || p_attach_name || '"' || UTL_TCP.crlf || UTL_TCP.crlf);
 
    FOR i IN 0 .. TRUNC((DBMS_LOB.getlength(p_attach_clob) - 1 )/l_step) LOOP
      UTL_SMTP.write_data(l_mail_conn, DBMS_LOB.substr(p_attach_clob, l_step, i * l_step + 1));
    END LOOP;

    UTL_SMTP.write_data(l_mail_conn, UTL_TCP.crlf || UTL_TCP.crlf);
  END IF;
  
  UTL_SMTP.write_data(l_mail_conn, '--' || l_boundary || '--' || UTL_TCP.crlf);
  UTL_SMTP.close_data(l_mail_conn);

  UTL_SMTP.quit(l_mail_conn);
END;
/

The code below shows how the procedure is called.

DECLARE
  l_clob CLOB := 'This is a very small CLOB!';
BEGIN
  send_mail(p_to          => 'me@mycompany.com',
            p_from        => 'admin@mycompany.com',
            p_subject     => 'Test Message',
            p_text_msg    => 'This is a test message.',
            p_attach_name => 'test.txt',
            p_attach_mime => 'text/plain',
            p_attach_clob => l_clob,
            p_smtp_host   => 'smtp.mycompany.com');
END;
/

Multiple Recipients

When dealing with multiple recipients, the UTL_SMTP.RCPT procedure needs to be called for each recipient, whether they are a "TO", "CC" or "BCC". The destinction between the types of recipient is made in the descriptions in the WRITE_DATA calls. The following procedure accepts comma separated "TO", "CC" and "BCC" parameters. The "TO" is mandatory, but the others are optional. If present, they are processed appropriately by splitting the strings up using the string_api package.

CREATE OR REPLACE PROCEDURE send_mail (p_to        IN VARCHAR2,
                                       p_cc        IN VARCHAR2 DEFAULT NULL,
                                       p_bcc       IN VARCHAR2 DEFAULT NULL,
                                       p_from      IN VARCHAR2,
                                       p_subject   IN VARCHAR2,
                                       p_message   IN VARCHAR2,
                                       p_smtp_host IN VARCHAR2,
                                       p_smtp_port IN NUMBER DEFAULT 25)
AS
  l_mail_conn   UTL_SMTP.connection;
  
  PROCEDURE process_recipients(p_mail_conn IN OUT UTL_SMTP.connection,
                               p_list      IN     VARCHAR2)
  AS
    l_tab string_api.t_split_array;
  BEGIN
    IF TRIM(p_list) IS NOT NULL THEN
      l_tab := string_api.split_text(p_list);
      FOR i IN 1 .. l_tab.COUNT LOOP
        UTL_SMTP.rcpt(p_mail_conn, TRIM(l_tab(i)));
      END LOOP;
    END IF;
  END;
BEGIN
  l_mail_conn := UTL_SMTP.open_connection(p_smtp_host, p_smtp_port);
  UTL_SMTP.helo(l_mail_conn, p_smtp_host);
  UTL_SMTP.mail(l_mail_conn, p_from);
  process_recipients(l_mail_conn, p_to);
  process_recipients(l_mail_conn, p_cc);
  process_recipients(l_mail_conn, p_bcc);

  UTL_SMTP.open_data(l_mail_conn);
  
  UTL_SMTP.write_data(l_mail_conn, 'Date: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS') || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'To: ' || p_to || UTL_TCP.crlf);
  IF TRIM(p_cc) IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, 'CC: ' || REPLACE(p_cc, ',', ';') || UTL_TCP.crlf);
  END IF;
  IF TRIM(p_bcc) IS NOT NULL THEN
    UTL_SMTP.write_data(l_mail_conn, 'BCC: ' || REPLACE(p_bcc, ',', ';') || UTL_TCP.crlf);
  END IF;
  UTL_SMTP.write_data(l_mail_conn, 'From: ' || p_from || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Subject: ' || p_subject || UTL_TCP.crlf);
  UTL_SMTP.write_data(l_mail_conn, 'Reply-To: ' || p_from || UTL_TCP.crlf || UTL_TCP.crlf);
  
  UTL_SMTP.write_data(l_mail_conn, p_message || UTL_TCP.crlf || UTL_TCP.crlf);
  UTL_SMTP.close_data(l_mail_conn);

  UTL_SMTP.quit(l_mail_conn);
END;
/

Miscellaneous

The UTL_SMTP package requires Jserver which can be installed by running the following scripts as SYS.

SQL> @$ORACLE_HOME/javavm/install/initjvm.sql
SQL> @$ORACLE_HOME/rdbms/admin/initplsj.sql

If you are attempting to use this method from Oracle 11g, you must remember to configure fine grained access to network services for the mail server.





2022년 8월 31일 수요일

OS별 서버 모델명 확인 방법

[ 출처 : https://onedaystudy.tistory.com/ ]


OS별 명령어로 확인하는 방법이다.

Solaris
# prtconf -vp | grep banner-name

AIX
# prtconf | grep Model(한글일 경우 '모델')

HP-UX
# model

LINUX
# dmidecode | grep Name

Windows Server
# wmic computersystem get model, name, manufacturer, systemtype

2022년 6월 29일 수요일

Amavisd 메일 필터 설정

 [ 참조 : https://linuking.com/ ]

Spamassassin score system

X-Spam-Status

스팸 스코어가 무슨 테스트로 인해 지정되었는지를 설명하고 있다.

아래 예는 No - 스팸 메시지가 아니다. hits=2.2 태그 레벨이 2.2로 HTMl, SPF 테스트를 수행했다는 의미를 포함하고 있다.

X-ASF-Spam-Status: No, hits=2.2 required=10.0   tests=HTML_MESSAGE,SPF_PASS

Amavisd 에서 spam filtering 수준 조절

Amavisd 를 이용해 원하는 태그, 아래와 같은 “spamminess” level 을 지정해 줄 수 있다.

 

$sa_tag_level_deflt= 2.0;

X-Spam-Status, X-Spam-Level 태그에 스팸 레벨을 지정한다. $sa_tag_level_deflt= undef; 로 지정하면 이 태그는 항상 추가된다.

 

$sa_tag2_level_deflt = 6.31;

스팸이란 판단의 경계 스코어 값이다.  이 값 이상의 스코어를 얻은 메시지는 스팸으로 인식되고 메시지 제목에 **** SPAM **** 이 추가된다. 6.31이라는 값은 너무 높은 것 같고 $sa_tag2_level_deflt = 5.0으로 사용해 보라.

 

$sa_kill_level_deflt = 6.31;

스팸 레벨이 이 값일 경우 Amavis에 의해 메일은 검역소로 보내지거나 특정 메일 박스로 간다. 또한 메시지의 “spamminess” level 을 정의해서 스팸 처리된 메일의 송신자는 Delivery Status Notification (DSN) 인 “your message was not delivered” 와 같은 이메일을 받을 수 있다. 단, $sa_dsn_cutoff_level value 이 스팸 레벨 이하로 설정되면 DSN 메시지는 보내지 않는다.

 

$sa_kill_level_deflt = 10000; 로 설정하면 스패머에게 메일 전송을 안하고 특정 메일박스에 복사하지도 않는다. 즉 사용자에게 특정한 문자가 추가된 메일을 전송하게 된다. 이 숫자는 10000을 넘지 않는게 좋겠다.

$sa_tag2_level_deflt 를 kill_level_deflt와 같은 값으로 설정하면 특정 메일박스로 가고 사용자에게 전송되지 않는다. 바로 이어서 “$spam_quarantine_to” 에 특정 이메일 주소를 명시해서 스팸으로 인식( 4개의 스팸 헤더, 제목 변경 혹은 검역소로 보내야하는)되는 메시지를 보내거나 이 값이 없다면 시스템의 검역소로 보내진다.

 

$sa_dsn_cutoff_level = 9;

DSN 메시지를 메일 발송자에게 전송하지 않는 스팸 레벨을 정의한다. You can leave it as it is, since we’ll not do any bouncing (see below the change from D_BOUNCE to D_DISCARD).

 

@bypass_virus_checks_maps = (1);

Anti-Virus 검증을 제외한다.

 

$sa_spam_subject_tag = '***SPAM*** ';

메일 메시지가 스팸으로 판정되면 메일 제목의 앞에 추가하는 문자열. 마지막 공백이 있음에 주의.

 

@local_domains_maps = ( [".$mydomain"] );

주어진 도메인을 Amavis 가 이메일을 스캔할 것을 정의. 아래와 같이 지역 도메인을 사용할 수 있다.

@local_domains_maps = ( [".$mydomain", "example2.com", "example3.com"] );

 

# $final_virus_destiny= D_DISCARD;

이 라인을 찾아 # 로 막았으면 제거한다. Amavis 가 검역소로 바이러스가 포함된 메시지를 전송한다 ( 특정한 이메일 박스, 시스템에 지정된 검역소  )

 

$final_*_destiny 의 값을 D_DISCARD 로 설정하면 메일 발송자에게 바이러스 차단에 대한 메시지를 전송하지 않는다.

 

$final_banned_destiny=D_DISCARD;

Amavis 가 금지된 파일을로 무엇을 할지 지정한다. D_DISCARD 로 지정하면 지정된 메일박스로 메시지를 전달할 수 이다.

기본은 D_BOUNCE.

Note: even if you leave this setting as is, i.e. commented out, double-extension files will still be blocked. This is an Amavis default.

 

$final_spam_destiny= D_DISCARD;

스팸 메일을 검역소로 보낸다 - $spam_quarantine_to 에 지정된 이메일 주소. 기본은 D_BOUNCE

 

 

$virus_admin= "virusalert@$mydomain";

바이러스 발견시 메시지를 전송할 관리자 이메일 postmaster 를 사용하자.

$virus_admin= "postmaster@$mydomain";

 

 

경고 등 관리자는 postmaster 를 이용하자:

$mailfrom_notify_admin= "postmaster@$mydomain";

$mailfrom_notify_recip= "postmaster@$mydomain";

$mailfrom_notify_spamadmin = "postmaster@$mydomain";

즉, FROM: postmaster@example.com 로 사용자에게 메시지가 전달된다.

 

$spam_admin 설정이 있지만 스팸 발견시 경고 메시지를 전송하지는 말자.

 

아래 명령들은 amavis 설정 파일에 기본적으로 준비되어 있지 않다. 입력을 하길 바란다.

@bypass_banned_checks_maps = (1);

특정한 확장자를 포함한 이메일은 금지한다 - 모든 이중 확장자 파일은 막는다.

금지할 확장자 들은 $banned_filename_re = new_RE 에서 정의한다.

 

금지 파일

$banned_filename_re = new_RE(

# qr'^UNDECIPHERABLE$',# is or contains any undecipherable components

 

# block certain double extensions anywhere in the base name

qr'.[^./]*[A-Za-z][^./]*.(exe|vbs|pif|scr|bat|cmd|com|cpl|dll).?$'i,

 

# qr'{[0-9a-z]{4,}(-[0-9a-z]{4,}){0,7}}?'i,# Class ID extensions - CLSID

 

qr'^application/x-msdownload$'i,# block these MIME types

qr'^application/x-msdos-program$'i,

qr'^application/hta$'i,

 

# qr'^message/partial$'i,# rfc2046 MIME type

# qr'^message/external-body$'i,# rfc2046 MIME type

 

# [ qr'^.(Z|gz|bz2)$'=> 0 ],# allow any in Unix-compressed

[ qr'^.(rpm|cpio|tar)$'=> 0 ],# allow any in Unix-type archives

# [ qr'^.(zip|rar|arc|arj|zoo)$'=> 0 ],# allow any within such archives

 

qr'..(exe|vbs|pif|scr|bat|cmd|com|cpl)$'i, # banned extension - basic

# qr'..(ade|adp|app|bas|bat|chm|cmd|com|cpl|crt|emf|exe|fxp|grp|hlp|hta|

#inf|ins|isp|js|jse|lnk|mda|mdb|mde|mdw|mdt|mdz|msc|msi|msp|mst|

#ops|pcd|pif|prg|reg|scr|sct|shb|shs|vb|vbe|vbs|

#wmf|wsc|wsf|wsh)$'ix,# banned ext - long

 

# qr'..(mim|b64|bhx|hqx|xxe|uu|uue)$'i,# banned extension - WinZip vulnerab.

 

qr'^.(exe-ms)$',# banned file(1) types

# qr'^.(exe|lha|tnef|cab|dll)$',# banned file(1) types

);

 

@whitelist_sender_maps = read_hash("$MYHOME/white.lst");

white-list 파일이 있는 위치를 지정한다.기본값은 /var/amavis.

 

@blacklist_sender_maps = read_hash("$MYHOME/black.lst");

black-list 송신자가 있는 위치를 지정한다. 기본값은 /var/amavis.

 

$spam_quarantine_to = "spam@$mydomain";

$sa_kill_level_deflt 에서 지정한 스팸 레벨로 검출한 스팸  메시지를 전달한 이메일 주소. 만약 스팸으로 처리된 메시지를 사용자에게 Spam tag만 붙여 전송하고자 한다면 이 명령은 무시해도 좋다. 그렇지만 $sa_kill_level_deflt 과 $sa_tag2_level_deflt 레벨이 동일하다면 $spam_quarantine_to 설정을 해야 한다.

 

$virus_quarantine_to = "virus@$mydomain";

모든 바이러스 메일을 지정한 이메일로 전달한다.

 

$banned_quarantine_to = "spam@$mydomain";

지정한 파일 형식 ($bypass_banned_checks_maps 에서)을 금지하는 설정을 하고 해당 메일을 지정한 이메일로 전송한다.

 

$recipient_delimiter = '+';

or

$recipient_delimiter = '-';

Postfix 에서 이메일 주소에서 구분자를 지정했다면 둘 중 하나를 설정한다.

 

$hdrfrom_notify_admin = "Content Filter <postmaster@$mydomain>";

이 명령은 이메일 헤더의 FROM을 강제로 지정한 주소로 바꾸어준다.

 

Outgoing email 에 spam 해제

I think the easiest way to bypass spam scanning for outgoing emails is to set

     @bypass_spam_checks_maps = ( ["example.com"] );

 Or if you want to read your domains from a file:

     read_hash(%local_domains, '/etc/amavis/local_domains');

Then you can add your domains to /etc/amavis/local_domains - one domain per line.

You can use the same syntax for bypass virus/header/banned checks.

 

2022년 5월 6일 금요일

[SAP] How Subcontracting Cockpit ME2ON creates SD delivery?

 [ 출처 : https://blogs.sap.com/ ]


Purpose:

This blog is aimed at establishing a configuration and master data model to manifest how SD delivery is created from a Subcontracting Purchase order.

A delivery is a communication object especially from ERP to EWM and if ERP fails to create delivery, further warehouse activity is hindered it becomes mandatory to troubleshoot the reason for failure of delivery creation.

There are certain reason why customers choose to create delivery via shipping in case of Subcontracting Purchase order.

    Storage location is EWM managed,
    Storage location is HU manged in traditional HU model, the components which are sent to vendor are packed in Handling units, and HUs can only packed in deliveries not in material documents, so there are reasons to pick SD delivery in case of Subcontracting Purchase orders.

Mechanism of SD delivery creation:

    A sales organization, a distribution channel and a division must be assigned to the plant from which the components should be delivered. Path: Materials Management–>Purchasing–>Set up Stock Transport Order–>Define Shipping Data for Plants

    The subcontracting component must be created for the sales organization, the distribution channel and the division of the delivering plant in material master.

    A customer must be assigned to the subcontracting vendor and vice versa.

 

        The customer must be created for the sales organization, distribution channel and division of the delivering plant.

     A delivery type must be be entered for the provision of materials. Subcontracting delivery type in standard is LB, you can customize as per your requirement.

The order required field must be set to L, delivery for subcontracting. Delivery split with warehouse number triggered delivery distribution to EWM .

     The partner determination procedure LB of delivery type YLB must have partner function as vendor and marked as mandatory in VOPA.

     A shipping point must be assigned to the combination of shipping condition(Customer Master), loading group(Material master) and plant(Customizing) in OVL2.

 
Result:

If all the above criteria is maintained, ME2ON or ME2O creates SD delivery

 1) ME2ON


2) Click on Create delivery.

3). System creates SD delivery for subcontracting components.

 
Hence the topic ends here. Please suggest if any improvement is required to make this blog more useful.

In my next blog, I will cover subcontracting with Handling units.


2022년 1월 24일 월요일

HSODBC를 이용하여 ORACLE에서 MSSQL로 DB LINK 하기

 [ 출처 : https://oracle.tistory.com/189 ]

Oracle DB가 Windows에 설치 되어 있어야 가능합니다.

제어판상의 ODBC 데이터 원본 관리자 -> 시스템 DSN에 추가(시스템 환경 32/64에 맞추어 설정 되어야 함)

Listener에 추가(10g일 경우 hsodbc, 11g일 경우 dg4odbc로 등록함)

SID_LIST_LISTENER =
  (SID_LIST =
    (SID_DESC =
      (SID_NAME = CLRExtProc)
      (ORACLE_HOME = E:\app\Administrator\product\11.2.0\dbhome_1)
      (PROGRAM = extproc)
      (ENVS = "EXTPROC_DLLS=ONLY:E:\app\Administrator\product\11.2.0\dbhome_1\bin\oraclr11.dll")
    )
    (SID_DESC =
      (SID_NAME = MSSQL )
      (ORACLE_HOME = E:\app\Administrator\product\11.2.0\dbhome_1)
      (PROGRAM = dg4odbc)
    )

  )


Tnsname.ora에추가

MSSQL =
 (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = IP )(PORT = 1521))
    (CONNECT_DATA = (SID = MSSQL ))
    (HS=OK)
  ) 


환경 설정 파일 새로 생성

E:\app\Administrator\product\11.2.0\dbhome_1\hs\admin\initMSSQL.ora

# This is a sample agent init file that contains the HS parameters that are
# needed for the Database Gateway for ODBC
#
# HS init parameters
#

HS_FDS_CONNECT_INFO = MSSQL
HS_FDS_TRACE_LEVEL = 1
HS_DB_DOMAIN = MSSQL
HS_DB_NAME = MSSQL

#
# Environment variables required for the non-Oracle system
#
#set <envvar>=<value>

설정이 완료되었으면 리스트 재 시작함.

lsnrctl stop / start

 

db link 생성

CREATE DATABASE LINK HSODBC
CONNECT TO user IDENTIFIED BY password USING tnsname


테이블 조회

SELECT * FROM table@hsodbc;