特定グループに所属するユーザにメールを配送する

ホスト内のユーザ全員宛てにメールを送る方法 - 101回目のコンパイルにてホスト内のユーザ全員にメールを送信する方法を紹介しました。
今日はもう少し送信する範囲を限定するため、グループに所属するユーザ全員にメールが配送される、いうなれば「グループ同報」とでも言うべき方法を考えてみました。

使用するMTAは前回同様、postfixを想定しています。以下の図のような流れで処理されます。(図中のpostfixコンポーネントはところどころ不正確な部分があると思いますが、ご容赦ください)

ここではあるグループ "hoge" に所属するグループ宛の送信先を "group-hoge" として話を進めます。この宛先は /etc/postfix/aliases.reg の記載する正規表現で変更できます。

  1. "group-hoge"宛てに送信されたメールはpostfixのlocalというコンポーネントに渡されます。
  2. localコンポーネントはaliasの情報を元に配送先を決定します。今回は特定のエイリアスではなく、group-xxxの形式の宛先を持つ物を処理させたいので正規表現エイリアスマップを使用します。
  3. エイリアスマップによって mail2groups.sh というプログラムに送られたメールは宛先情報からグループ名を取得し、対象のグループに所属するユーザに対してメールを個別に配送します。
  4. mail2groups.sh から sendmail コマンドに渡されたメールが個別のユーザに配送されます。

関係するファイルとその中身や編集内容は以下の通りです。

alias_maps = hash:/etc/aliases
    ↓
alias_maps = hash:/etc/aliases, regexp:/etc/postfix/aliases.reg
  • /etc/postfix/aliases.reg (新規)
/^group-.+(@.*)?$/ "|/bin/bash /usr/local/lib/mail2groups.sh"
  • /usr/local/lib/mail2groups.sh (新規)
#!/bin/bash

USERID_MIN=500
USERID_MAX=29999


function isGroupOf()
{
    if [ ${#} -lt 2 -o -z "${2}" ]
    then
        false
        return
    fi
    
    if [ -n "`groups ${1} | awk -F: '{printf \$2}' | grep ${2}`" ]
    then
        true
    else
        false
    fi
}

declare -i USERID=0
TO=""
WILL_BE_SENT=0

while read USER_ENT
do
    USERID="`echo ${USER_ENT} | awk -F: '{print \$3;}'`"
    if [ ${USERID_MIN} -le ${USERID} -a ${USERID} -le ${USERID_MAX} ]
    then
        USERNAME="`echo ${USER_ENT} | awk -F: '{print $1;}'`"
        RCPT_GROUPNAME="${LOCAL#group-}"
        isGroupOf "${USERNAME}" "${RCPT_GROUPNAME}"
        if [ ${?} -eq 0 ]
        then
            TO="${TO} `echo "${USER_ENT}" | awk -F: '{printf $1;}'`"
            WILL_BE_SENT=1
        fi
    fi
done </etc/passwd

if [ ${WILL_BE_SENT} -ne 0 ]
then
    /usr/lib/sendmail $@ ${TO}
else
    # Undelivered Mail by "unknown user"
    exit 67
fi

全てのファイルの準備ができたら /etc/init.d/postfix reload を実行してpostfixをリロードします。
これでユーザグループ宛てのメール送信ができるようになっているはずです。

ちなみに「特定のグループに所属するユーザの一覧」を一度に取得する方法が見当たらなかったため、上記の /usr/local/lib/mail2groups.sh では一般ユーザのUID上限と下限を仮定してその範囲のユーザ全員に対してグループ所属の判定をしているため効率が悪いです。もしそのそういう判定をコマンド一発とかでできる方法をご存知の方がいれば教えていただけると幸いです。

MySQLバックアップツール "MySQL ZRM"

MySQLデータのバックアップ方法 | OSDN Magazineにて紹介されていたMySQL用のバックアップツール
Zmanda Recovery Manager for MySQL を使ってみることにしました。Zmanda Recovery Manager for MySQLの特徴は上記のSourceForgeの記事の下の方で紹介している箇所をご覧ください。

インストール

RedHat系用ではRPMファイルが配布されているのでwgetしてインストールします。

wget http://www.zmanda.com/downloads/community/ZRM-MySQL/2.2/RPM/MySQL-zrm-2.2.0-1.noarch.rpm
rpm -ivh MySQL-zrm-2.2.0-1.noarch.rpm

Debian系はMySQLデータのバックアップ方法 | OSDN Magazineに書いてあるようにhttp://www.howtoforge.com/mysql_zrm_debian_sargeを参照するといいとか。
MySQL-ZRMでは「バックアップセット」という単位でバックアップが実行されます。
ここでは"MyBackupSet"という名前のバックアップセットを作っておきます。(名前は適宜変更してください)

mkdir /etc/mysql-zrm/MyBackupSet/
cp /etc/mysql-zrm/mysql-zrm.conf /etc/mysql-zrm/MyBackupSet/
chown mysql:mysql -R /etc/mysql-zrm/MyBackupSet/
chmod 775 /etc/mysql-zrm/MyBackupSet/
chmod 644 /etc/mysql-zrm/MyBackupSet/mysql-zrm.conf

インストールしたらバックアップ処理専用のDBユーザを作っておきましょう。
私の場合は"backup_operator"という名前で、SELECT, RELOAD, SHUTDOWN, SUPER, LOCK TABLES, REPLICATION CLIENT のグローバル権限を付与したユーザをバックアップ処理用ユーザとしました。

バックアップを実行する

バックアップを実行するときにはmysql-zrm-backupコマンドを使います。以下のように、実行するバックアップセットの名前を指定する必要があります。(省略時は"BackupSet1"が使用されます)

mysql-zrm-backup --backup-set=MyBackupSet
差分バックアップを取得する

差分バックアップを取得するためにはバイナリログが有効になっている必要があります。
/etc/my.cnfの[mysqld]セクションに以下の二行を追加してからmysqldを再起動しましょう。(パスや値は適宜変更してください)

log_bin=/var/lib/mysql/mysqld-bin
max_binlog_size = 512M

mysql-zrm-backupコマンドはバイナリログの場所をmy.cnfからは読み取らないので、my.cnfでmysql-zrm-backupのデフォルト値(/var/mysql/mysqld-bin)以外を指定する場合はそのパスをmysql-zrm.confに書くか、--mysql-binlog-pathオプションで指定する必要があります。
参考:
http://dev.mysql.com/doc/refman/5.1/ja/binary-log.html
http://open-groove.net/mysql/mysql-binlog/

バックアップデータを暗号化する

バックアップデータを暗号化して保存する場合はmysql-zrm.conf内で

encrypt=1

の行を有効にして、パスフレーズファイルとして /etc/mysql-zrm/.passphrase (中身は任意の文字列)を用意しておく必要があります。

echo -n hogemoge > /etc/mysql-zrm/.passphrase
chmod 600 /etc/mysql-zrm/.passphrase
chown mysql:mysql /etc/mysql-zrm/.passphrase

パスフレーズファイルのパーミッションと紛失には注意してください。

バックアップデータから復元する

復元する時には

mysql-zrm-restore --source-directory \
        /var/lib/mysql-zrm/MyBackupSet/20100517020503/
mysql-zrm-verify-backup --source-directory \
        /var/lib/mysql-zrm/MyBackupSet/20100517020503/

といった形で復元元データが格納されたディレクトリを指定して復旧と検証を行います。
バックアップからの復旧完了後、mysqldが停止した状態で完了するのでmysqldを再起動して復旧は完了です。

バックアップをスケジュールする

簡単なものであればmysql-zrm-schedulerコマンドで登録することができます。(実際はコマンド内からcronに登録しているだけのようです。)
具体的な使用方法はmanページをご覧ください。

手動でスケジュールを組みたい場合は自分でcrontabを編集してもいいでしょう。
以下は

を行う場合の設定例です。

# Full backup of mysql database on Every Monday 08:00
00 8 * * 1 root mysql-zrm-backup --backup-set MyBackupSet --backup-level 0 --backup-name "$(date '+\%Y-\%m-\%d')f"

# Incremental backup of mysql database on Every Monday-Friday 23:50
50 23 * * 1-5 root mysql-zrm-backup --backup-set MyBackupSet --backup-level 1 --backup-name "$(date +\%Y-\%m-\%d)i"

フルバックアップには保存先のディレクトリ名に "f" を、差分バックアップの場合は "i" を付加してあります。(バックアップ実行時に保存先のディレクトリがすでに存在する場合、エラーとなってしまうので注意してください。)

WEBrickでアップロードされたファイルを保存する

思いつきでRubyをすこしつまみ食いしてみました。Ruby初体験です。
Ruby+WEBrickをチョコチョコといじる過程でアップロードされたファイルを保存するサーブレットを作ってみました。意外と用途はありそうなんですがサンプルがぜんぜん見つからなかったので同じようなことやりたい人向けに公開しておきます。(探し方が悪い可能性は否めませんが…)
ただRuby歴1日の人が書いたコードなので「お作法」に従ってない可能性がありますが、その点ご了承ください。むしろ指摘していただければ助かります。f(^^;)

uploader.rb

require 'webrick'

class Uploader < WEBrick::HTTPServlet::AbstractServlet
    def do_POST(request, response)
        print 'HTTP Method: POST', "\n"
        print 'HTTP Content-Type: ', request.query['uploadedFile']['content-type'], "\n"
        print 'File name: ', request.query['uploadedFile'].filename, "\n"
        print 'Data size: ', request.query['uploadedFile'].to_s.size, "\n"
        
        filename = request.query['uploadedFile'].filename
        uploadedFile = File.open(filename, "wb")
        uploadedFile.write request.query['uploadedFile'].to_s
        uploadedFile.close
    end
    
    def do_GET(request, response)
        print 'HTTP Method: GET', "\n"
    end
end

上記のrbファイルをサーブレットとしてHTTPサーバにマッピングしたら受け側は準備完了です。下記コードでは /upload をマッピング先としました。またアップロードされたファイルは元のファイル名でカレントディレクトリに保存されます。(本当はドキュメントルートのパスをHTTPServerのインスタンスから取得してそこに配置するようにしたかったのですが、やり方がわかりませんでした。)
httpserver.rb

require 'webrick'
include WEBrick

server = HTTPServer.new(
    :Port => 8000,
    :DocumentRoot => File.join(Dir::pwd, "public_html")
)
trap("INT"){ server.shutdown }

require 'uploader'
server.mount('/upload', Uploader)

server.start

あとは以下のような形でformタグを書いてアップロードするフォームを用意したHTMLファイルをpublic_htmlにでも入れておけばそこからアップロードできるようになります。

<form method="POST" action="/upload" enctype="multipart/form-data">
    <div>
        アップロードするファイル:
        <input type="file" name="uploadedFile" size="100"/>
    </div>
    <input type="submit" value="アップロード"/>
    <input type="reset" value="取消"/>
</form>

PHPでパワーポイントをPNGにする

RubyでパワーポイントのスライドをPNGにする。 - それマグで!で紹介されていたやり方をPHPに翻訳してみました。
PHPのCOM拡張を使用しています。

com_ppt2png.php

<?php
require_once 'EventSink_EApplication.php';

function ConvertPPT2Image($fileName)
{
    //COMインスタンス作成し、タイプライブラリをロード
    $ppt_com = new COM('PowerPoint.Application');
    if (!com_load_typelib('PowerPoint.Application'))
    {
        throw new Exception("Cannot load type library of 'PowerPoint.Application'");
    }

    //アプリケーション起動
    $ppt_com->Visible = 1;

    //イベントシンクオブジェクトに接続
//    $ppt_AppEventSink = new EApplication();
//    com_event_sink($ppt_com, $ppt_AppEventSink, 'EApplication');

    //ドキュメント変換
    $presentation = $ppt_com->Presentations->Open(realpath($fileName));
    $presentation->SaveAs('\slides.png', ppSaveAsPNG);
    $presentation->Close();

    //アプリケーション終了
    $ppt_com->Quit();
}

//main()
try
{
    ConvertPPT2Image($argv[1]);
}
catch(Exception $e)
{
    print $e . "\n";
    debug_print_backtrace();
    die();
}
?>

スクリプトを保存して、第1引数に変換するpptファイルを指定して実行してください。実行するとルートディレクトリにslidesというディレクトりが生成され、その中に「スライド1.png スライド2.png ...」 というファイル名で画像が出力されます。
イベントシンクオブジェクトのテンプレートは com_print_typeinfo()関数 で生成可能です。私はこんな感じでコマンドプロンプトからテンプレートを出力させました。(出力されるコードは<?php 〜 ?>で囲まれていないので追加する必要があります。)

php -r "com_print_typeinfo(new COM('PowerPoint.Application'), 'EApplication');" >EventSink_EApplication.php

上記コード中のイベントシンクオブジェクトへの接続箇所をコメントインして動作させるとこんなwarningが出力されました。

Warning: That's not a dispatchable interface!! type kind = 00000003 in C:\ppt2image\com_ppt2png.php on line 18

参照元記事に書かれていたのでPHP版でも書いてみましたが、イベントシンクオブジェクトへの接続はしなくても動作するようです。クローズイベントなどを拾いたいのでなければ特に必要ないのではないかと思いますが、なぜwarningが出るのかは不明です。ちゃんとしたやり方をご存知な方は教えていただけるとありがたいです。

ホスト内のユーザ全員宛てにメールを送る方法

背景

集団開発環境でコミットログなどをメールで通知する事が良くあると思いますが、その際に専用のMLを作成してしまうとML加入ユーザと開発に関係するユーザが別管理になってしまうので管理上わずらわしいです。なのでいっそ開発で使うサーバをPOPサーバにしてしまい、そこに通知メールを配送すればMLのユーザ管理とサーバのユーザ管理が同期するので手間が減らせるのでは?と考えました。

ホスト内のユーザにメールを配信するに先立ち、まずは単純に「いわゆるログオンユーザ」のリストを取得することを考えました。リストさえ取得できれば後はそのユーザに配送するだけなので、このリストアップ処理が要と考えた為です。

ユーザのリストアップ方法

  1. UIDの範囲を元に/etc/passwdファイルから取得
  2. /home 以下のディレクトリをリストアップする。
    • /home/storage や /home/share など、ユーザホームでないディレクトリが存在する場合がある。
    • Maildirを使用している場合は /home/*/Maildir の有無を調べれば送信の可否は調べられるが、万能ではない気がする…。
  3. passwdファイルのシェルに /sbin/nologin や/bin/false がセットされていないユーザをリストアップする
    • 確認してみると、デーモンを動作させるユーザでもログインシェルがセットされるものがあり、取得できるユーザリストにノイズが多い。

最初の選択肢がもっとも可搬でノイズが少なそうなので採用することにしました。案に基づきユーザをリストアップするスクリプト listup_users.sh を書いてみました。(UIDの上限と下限はそれぞれFedora系とDebian系どちらかの小さいほうで統一してあります。)

#!/bin/bash

USERID_MIN=500
USERID_MAX=29999

declare -i USERID=0

while read USER_ENT
do
    USERID="`echo ${USER_ENT} | awk -F: '{print \$3;}'`"
    if [ ${USERID_MIN} -le ${USERID} -a ${USERID} -le ${USERID_MAX} ]
    then
        echo "${USER_ENT}" | awk -F: '{printf $1" ";}'
    fi
done </etc/passwd

実行してみると以下のような出力が得られます。(出力末尾には改行がつきません)

[haruki]$ ./listup_users.sh
haruki family 

メール配送

リストアップができるようになったので、/var/lib/semdmail に引数としてリストアップしたユーザのリストを渡すことで送信可能できます。配信にはリストアップとsendmail呼び出しを1つのスクリプト内にまとめた mail2localusers.sh を用意することにしました。

#!/bin/bash

USERID_MIN=500
USERID_MAX=29999

declare -i USERID=0
TO=""

while read USER_ENT
do
    USERID="`echo ${USER_ENT} | awk -F: '{print \$3;}'`"
    if [ ${USERID_MIN} -le ${USERID} -a ${USERID} -le ${USERID_MAX} ]
    then
        TO="${TO} `echo "${USER_ENT}" | awk -F: '{printf $1;}'`"
    fi
done </etc/passwd

/usr/lib/sendmail $@ ${TO}

配置先は/usr/local/lib、所有者はroot:root、権限は0755としました。(/usr/local/libに配置したのはsendmailプログラムの配置に倣ったためです。)
ホスト内のユーザ全員に送信するというアドレスを作成したい場合はaliasを用意してパイプで上記のコマンドに渡してやると良いでしょう。aliasの設定方法は以下のページを参考にしました。
postfix :: aliases (エイリアス) の設定 [Tipsというかメモ]
Homeserver on Linux – 自宅サーバーを作ろう | Just another WordPress site

/etc/aliasの記述(postfixの例)

all-users:      | "/usr/local/lib/mail2localusers.sh"

更新後newaliasesコマンドを実行するのを忘れないようにしましょう。alias更新後、以下のコマンドを実行してユーザ全員に配送されていることを確認してみます。

echo "test for \"all-users\"" | mail all-users

アラインメントを気にしない読み出しを行う

仕事でARMアーキテクチャを使っているときにアラインメントをまたぐ読み出しを行ってプログラムが異常終了するケースがありました。
そのときは ARM gcc ¥Ð¥Ã¥É¥Î¥¦¥Ï¥¦½¸: ¥¢¥é¥¤¥ó¥á¥ó¥È に書いてあるようなバッドノウハウで切り抜けましたが、そもそもアラインメントを気にしない読み出しを行う関数があるといいのでは?と思い、読み出しを行うC++のテンプレート関数を書いてみました。

続きを読む