2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TWSNMPのポーリングを拡張するためにGo言語でIMAP/POP3サーバーのメール状況を確認するCLIツールを作ってみた

Posted at

はじめに

TWSNMPのポーリングにメールの受信確認を追加するために試作した、メールサーバーの動作確認やメールボックスの状況(メール数、サイズ、最新メールの日時など)を簡単にチェックできるCLIツールを紹介します。
Go言語で実装されており、IMAPとPOP3の両方に対応しています。

TWSNMPは、

です。

特徴

  • マルチプロトコル対応: IMAP (imap, imaps) および POP3 (pop3, pop3s) をサポートしています。
  • シンプルな操作: 接続情報をURL形式の引数として渡すだけで実行できます。
  • 詳細なステータス取得:
    • メールボックス内のメッセージ数
    • 合計サイズ
    • 最新メッセージの日時
    • 最新メッセージのID (UID)

ソースコード

全ソースコード(展開するとみられます)
package main

import (
	"fmt"
	"log"
	"net/mail"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"
	"github.com/knadh/go-pop3"
)

func main() {
	// コマンドライン引数のチェック
	if len(os.Args) < 2 {
		log.Fatalln("usage mailpolling url")
	}
	url := os.Args[1]
	var err error
	// URLのプロトコルに応じて処理を分岐
	if strings.HasPrefix(url, "imap") {
		err = imapPolling(url)
	} else if strings.HasPrefix(url, "pop3") {
		err = pop3Polling(url)
	}
	if err != nil {
		log.Fatalln(err)
	}
}

// IMAPプロトコルによるポーリング処理
func imapPolling(source string) error {
	// URLのパース
	u, err := url.Parse(source)
	if err != nil {
		return fmt.Errorf("invalid url: %w", err)
	}
	server := u.Host
	// ポート番号が指定されていない場合のデフォルト設定
	if !strings.Contains(server, ":") {
		if u.Scheme == "imaps" {
			server += ":993"
		} else {
			server += ":143"
		}
	}
	var c *imapclient.Client
	// 接続の確立 (TLSまたはStartTLS)
	if u.Scheme == "imaps" || strings.HasSuffix(server, ":993") {
		c, err = imapclient.DialTLS(server, nil)
	} else {
		c, err = imapclient.DialStartTLS(server, nil)
		if err != nil {
			// StartTLSが失敗した場合は、非セキュアな接続を試行
			c, err = imapclient.DialInsecure(server, nil)
		}
	}
	if err != nil {
		return fmt.Errorf("connection failed: %w", err)
	}
	defer c.Logout()

	// 認証情報の取得
	username := ""
	password := ""
	if u.User != nil {
		username = u.User.Username()
		if p, ok := u.User.Password(); ok {
			password = p
		}
	}
	// ログイン
	if err := c.Login(username, password).Wait(); err != nil {
		return fmt.Errorf("login failed: %w", err)
	}

	// メールボックスの選択 (デフォルトはINBOX)
	mbox := "INBOX"
	if len(u.Path) > 1 {
		mbox = u.Path[1:]
	}
	status, err := c.Select(mbox, nil).Wait()
	if err != nil {
		return fmt.Errorf("select failed: %w", err)
	}

	var totalSize int64
	var lastDate time.Time
	var lastID string
	// メッセージが存在する場合、詳細情報を取得
	if status.NumMessages > 0 {
		var seqSet imap.SeqSet
		seqSet.AddRange(1, status.NumMessages)
		fetchOptions := &imap.FetchOptions{RFC822Size: true, InternalDate: true, UID: true}
		messages, err := c.Fetch(seqSet, fetchOptions).Collect()
		if err != nil {
			log.Printf("fetch error: %v", err)
		} else {
			// 全メッセージのサイズを合算
			for _, msg := range messages {
				totalSize += msg.RFC822Size
			}
			// 最終メッセージの情報を取得
			if len(messages) > 0 {
				lastMsg := messages[len(messages)-1]
				lastDate = lastMsg.InternalDate
				lastID = strconv.FormatUint(uint64(lastMsg.UID), 10)
			}
		}
	}
	// 結果を出力
	log.Printf("count=%d size=%d last=%s id=%s err=%v", status.NumMessages, totalSize, lastDate.Format(time.RFC3339), lastID, nil)
	return nil
}

// POP3プロトコルによるポーリング処理
func pop3Polling(source string) error {
	// URLのパース
	u, err := url.Parse(source)
	if err != nil {
		return fmt.Errorf("invalid url: %w", err)
	}
	server := u.Hostname()
	port := u.Port()
	// ポート番号のデフォルト設定
	if port == "" {
		if u.Scheme == "pop3s" {
			port = "995"
		} else {
			port = "110"
		}
	}
	nport, err := strconv.Atoi(port)
	if err != nil {
		return fmt.Errorf("invalid port: %w", err)
	}

	// 認証情報の取得
	username := ""
	password := ""
	if u.User != nil {
		username = u.User.Username()
		if p, ok := u.User.Password(); ok {
			password = p
		}
	}

	// POP3クライアントの設定と接続
	p := pop3.New(pop3.Opt{
		Host:       server,
		Port:       nport,
		TLSEnabled: u.Scheme == "pop3s" || nport == 995,
	})
	conn, err := p.NewConn()
	if err != nil {
		return fmt.Errorf("connection failed: %w", err)
	}
	defer conn.Quit()

	// 認証
	if err := conn.Auth(username, password); err != nil {
		return fmt.Errorf("auth failed: %w", err)
	}

	// ステータス情報の取得 (メッセージ数と合計サイズ)
	count, size, err := conn.Stat()
	if err != nil {
		return fmt.Errorf("stat failed: %w", err)
	}

	var lastDate time.Time
	var lastID string
	if count > 0 {
		// 最終メッセージのUIDを取得
		if uidls, err := conn.Uidl(count); err == nil && len(uidls) > 0 {
			lastID = uidls[0].UID
		}
		// 最終メッセージのヘッダーから日付を取得
		if msg, err := conn.Top(count, 0); err == nil {
			dateStr := msg.Header.Get("Date")
			if t, err := mail.ParseDate(dateStr); err == nil {
				lastDate = t
			}
		}
	}

	// 結果を出力
	log.Printf("count=%d size=%d last=%s id=%s err=%v", count, size, lastDate.Format(time.RFC3339), lastID, nil)
	return nil
}


使用しているライブラリ

本ツールでは、以下のオープンソースライブラリを活用しています。

インストールとビルド

mkdir mailpolling
cd mailpolling
# main.goにソースコードを内容を保存してください。
go mod init mailpolling
go mod tidy
go build

使い方

実行ファイルに接続先URLを引数として渡して実行します。

構文

./mailpolling <URL>

URLの形式: protocol://user:password@host:port/path

  • protocol: imap, imaps, pop3, pop3s
  • path: IMAPの場合、メールボックス名(デフォルトはINBOX)

実行例

IMAP (SSL/TLS) の場合:

./mailpolling imaps://user:pass@imap.example.com:993/INBOX

POP3 (SSL/TLS) の場合:

./mailpolling pop3s://user:pass@pop.example.com:995

出力形式

実行結果は標準ログ出力として表示されます。エラーが発生した場合はエラーメッセージが出力されます。

2025/01/20 10:00:00 count=123 size=456789 last=2025-01-20T09:55:00Z id=12345 err=<nil>
  • count: メッセージ数
  • size: 合計サイズ(バイト)
  • last: 最新メッセージの日時
  • id: 最新メッセージのUID
  • err: エラー内容(成功時は <nil>

コードのポイント

メイン処理 (main.go)

URLのスキーム(プロトコル)を見て、IMAP用またはPOP3用の処理関数を呼び分けています。

func main() {
	if len(os.Args) < 2 {
		log.Fatalln("usage mailpolling url")
	}
	url := os.Args[1]
	// ... (中略) ...
	if strings.HasPrefix(url, "imap") {
		err = imapPolling(url)
	} else if strings.HasPrefix(url, "pop3") {
		err = pop3Polling(url)
	}
	// ...
}

IMAPポーリング (imapPolling)

emersion/go-imap/v2 を使用して実装しています。
DialTLSDialStartTLS を使い分け、セキュアな接続を優先します。
Fetch コマンドで全メッセージの RFC822Size, InternalDate, UID を取得し、集計しています。

// メッセージ情報の取得部分
fetchOptions := &imap.FetchOptions{RFC822Size: true, InternalDate: true, UID: true}
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
// ... 集計処理 ...

POP3ポーリング (pop3Polling)

knadh/go-pop3 を使用しています。
Stat コマンドで件数とサイズを取得し、UidlTop コマンドを組み合わせて最新メッセージのUIDと日付を取得しています。

// ステータスと最新情報の取得
count, size, err := conn.Stat()
// ...
if uidls, err := conn.Uidl(count); err == nil && len(uidls) > 0 {
    lastID = uidls[0].UID
}
if msg, err := conn.Top(count, 0); err == nil {
    // ヘッダーからDateをパース
}

まとめ

Go言語の強力な標準ライブラリとサードパーティライブラリを組み合わせることで、比較的少ないコード量で実用的なメールサーバー監視ツールを作成できました。
TWSNMPなどの監視システムと連携させることで、メールサーバーの死活監視だけでなく、メールの滞留や着信確認などにも活用できます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?