четверг, 13 ноября 2014 г.

Настройка GNU Mailman в качестве почтового релея

Как известно для работы GNU Mailman требуется также настройка почтового сервера (MTA) на той же машине на которой работает Mailman. При отправке писем Mailman связывается с локальным SMTP сервером. Для доставки писем в зависимости от используемого сервера как правило используется /etc/aliases для перенаправления писем в Mailman. Преимущества традиционного подхода в том что нет лишней нагрузки на сеть при получении писем. Т.е. письма приходящие на наш почтовый сервер будут автоматически перенаправлены в Mailman. Однако, при настройке почтового сервера появляются сопутствующие проблемы, такие как безопасность, доступность сервера в сети, спам и дополнительное администрирование. Настройка Mailman в качестве релея подразумевает использование удаленного SMTP/POP3(или IMAP) сервера. Такой подход имеет ряд преимуществ: 
  1. Не требуется доступность извне, т.е. ненужен белый IP-адрес.
  2. Не нужно настраивать полноценный почтовый сервер и отвечать за него.
  3. Отложенная доставка. В случае отключения Mailman письма будут оставаться на удаленном сервере. При включении они будут обработаны Mailman.
И ряд недостатков:
  1. Дополнительная нагрузка на сеть. Т.к. приходится опрашивать POP3/IMAP сервер на предмет наличия почты.
  2. Как следствие более длительная рассылка почты в зависимости от интервала опроса.

Установка Mailman

Далее произведем установку и подготовку пакета. Для примера настройки выбран дистрибутив Slackware64-14.1 и mailman 2.1.18-1. Использовались следующие зависимости в системе:
Для работы нам потребуется создать пользователя и группу:

 groupadd -g 204 mailman
 useradd -u 204 -d /home/mailman -s /bin/bash -g mailman mailman

Домашняя директорию я решил использовать в дальнейшем для хранения конфига fetchmail.
Качаем слакбилд и исходники mailman:

 cd /tmp/
 wget http://slackbuilds.org/slackbuilds/14.1/network/mailman.tar.gz
 tar zxvf ./mailman.tar.gz && cd mailman
 wget http://ftp.gnu.org/gnu/mailman/mailman-2.1.18-1.tgz

В моем случае слакбилд был для версии 2.1.17, таким образом пришлось модифицировать пару строк внутри:

 sed -i 's/\(VERSION:-\)[0-9.]*/\12.1.18-1/' ./mailman.SlackBuild
 sed -i '/makepkg/c\/sbin/makepkg -l y -c n $OUTPUT/$PRGNAM-${VERSION/-/_/}-$ARCH-$BUILD$TAG.${PKGTYPE:-tgz}' ./mailman.SlackBuild

Собираем пакет:
 MAIL_GID=mailman VAR_PREFIX="/var/mailman" ./mailman.SlackBuild

Если ранее были установлены все зависимости, пакет должен быть собран без проблем. Пакет подготовлен для установки в /opt/mailman.
Далее установка пакета:
 installpkg /tmp/mailman-2.1.18_1-x86_64-1_SBo.tgz

Теперь исправляем права доступа:
 сhgrp -R apache /var/mailman/archives/public
 cd /opt/mailman; ./bin/check_perms -f

Нам нужно добавить возможность использования удаленного SMTP сервера. Для этого необходимо изменить файл /opt/mailman/Mailman/Handlers/SMTPDirect.py
Так как в настройках в дальнейшем можно указать модуль для отправки, я предпочитаю делать копию и редактирование:
 cp /opt/mailman/Mailman/Handlers/SMTPDirect.py /opt/mailman/Mailman/Handlers/ASMTPDirect.py
 patch -d /opt/mailman/ -p0 -i `pwd`/ASMTPDirect.patch

Нажмите для просмотра файла ASMTPDirect.patch
--- Mailman/Handlers/ASMTPDirect.py 2014-10-29 18:09:11.729196543 +0600
+++ Mailman/Handlers/ASMTPDirect.py 2014-10-30 21:07:16.702143716 +0600
@@ -15,9 +15,9 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 # USA.
 
-"""Local SMTP direct drop-off.
+"""Local/Remote SMTP direct drop-off.
 
-This module delivers messages via SMTP to a locally specified daemon.  This
+This module delivers messages via SMTP to a local or remote server.  This
 should be compatible with any modern SMTP server.  It is expected that the MTA
 handles all final delivery.  We have to play tricks so that the list object
 isn't locked while delivery occurs synchronously.
@@ -44,6 +44,8 @@
 from email.Header import Header
 from email.Charset import Charset
 
+import re
+
 DOT = '.'
 
 try:
@@ -60,14 +62,23 @@
         self.__conn = None
 
     def __connect(self):
-        self.__conn = smtplib.SMTP()
-        self.__conn.connect(mm_cfg.SMTPHOST, mm_cfg.SMTPPORT)
+        if mm_cfg.SMTP_SSL:
+            self.__conn = smtplib.SMTP_SSL()
+        else:
+            self.__conn = smtplib.SMTP()
+        self.__conn.connect(mm_cfg.SMTP_HOST, mm_cfg.SMTP_PORT)
         self.__numsessions = mm_cfg.SMTP_MAX_SESSIONS_PER_CONNECTION
+        if mm_cfg.SMTP_STARTTLS:
+            self.__conn.starttls()
+        if mm_cfg.SMTP_AUTH:
+            self.__conn.login(mm_cfg.SMTP_USERNAME,mm_cfg.SMTP_PASSWORD)
 
     def sendmail(self, envsender, recips, msgtext):
         if self.__conn is None:
             self.__connect()
         try:
+#            syslog('smtp-failure',"\n%s\n",
+#                   msgtext)
             results = self.__conn.sendmail(envsender, recips, msgtext)
         except smtplib.SMTPException:
             # For safety, close this connection.  The next send attempt will
@@ -376,6 +387,18 @@
     if mlist.include_sender_header:
         del msg['sender']
         msg['Sender'] = '"%s" <%s>' % (mlist.real_name, envsender)
+    
+    # Set properly From header
+    
+    out_addr = mm_cfg.SMTP_USERNAME
+    p = re.compile(r'([-\w\.]+)@((?:\w+\.)+)(\w{2,4})')
+    old_from = msg['From']
+    if 'Reply-To' not in msg:
+        msg.add_header('Reply-To',old_from)
+    del msg['From']
+    msg['From'] = p.sub(out_addr,old_from)
+    envsender = out_addr
+    
     # Get the plain, flattened text of the message, sans unixfrom
     # using our as_string() method to not mangle From_ and not fold
     # sub-part headers possibly breaking signatures.

Теперь настроим доступ к веб-интерфейсу. Создадим файл /etc/httpd/mailman.conf:
Alias /pipermail/ /var/mailman/archives/public/
<Directory "/var/mailman/archives/public">
    Options Indexes FollowSymlinks
    AllowOverride None
    Require all granted
</Directory>

ScriptAlias /mailman/ /opt/mailman/cgi-bin/
<Location "/mailman/">
    Options +ExecCGI
    Require all granted
</Location>

Alias /mailman_icons/ /opt/mailman/icons/
<Directory "/opt/mailman/icons">
   Require all granted
</Directory>
И подключим его в /etc/httpd/httpd.conf:
# Access to mailman
#
Include /etc/httpd/mailman.conf
В httpd.conf также необходимо включить поддержку CGI.
LoadModule cgid_module lib64/httpd/modules/mod_cgid.so
LoadModule cgi_module lib64/httpd/modules/mod_cgi.so
В остальном Apache настраивается как обычно. Не забываем заставить httpd перечитать конфиг с нашими изменениями:
 /etc/rc.d/rc.httpd graceful

Настройка 

Для дальнейшей настройки необходимо иметь внешний почтовый сервер и аккаунт для отправки с которого будет осуществляться рассылка. Можно например запарковать свой домен на Яндексе. Предположим что у вас есть домен mydomain.ru который запаркован на Яндексе или на котором есть почтовый сервер. Теперь перейдем к настройке mailman. Рекомендуется посмотреть описание и настройки по умолчанию которые находятся в /opt/mailman/Mailman/Defaults.py. Все наши изменения настроек должны находится /opt/mailman/Mailman/mm_cfg.py. Пример настроек:
# Прежде всего необходимо прописать почтовый домен с которым работает Mailman:
DEFAULT_EMAIL_HOST = 'mydomain.ru'

# Укажем DNS имя сервера на котором работает веб интерфейс Mailman:
DEFAULT_URL_HOST = 'localhost'

# Дополнительно укажем язык по умолчанию и URL для иконок:
DEFAULT_SERVER_LANGUAGE = 'ru'
IMAGE_LOGOS = '/mailman_icons/'

# Укажем пропатченный модуль SMTP с применением аутентификации:
DELIVERY_MODULE = 'ASMTPDirect'

# SMTP сервер:
SMTP_TLS = Yes
SMTP_HOST = 'smtp.yandex.ru'
SMTP_PORT = 587

# Далее приведем настройки аутентификации аккаунта SMTP 
# от которого будут отправляться все письма Mailman:
SMTP_AUTH = Yes
SMTP_USERNAME = 'noreply'
SMTP_EMAIL_HOST = 'mydomain.ru'
SMTP_PASSWORD = 'mypass'

Далее укажем пароль к сайту:
 cd /opt/mailman
 ./bin/mmsitepass
Пароль администратора сайта можно использовать для создания новых списков через веб интерфейс. Также рассмотрим создание списка рассылки с консоли. Рассмотрим типичную структуру списка почтовых адресов в Mailman:

аккаунт описание
somelist отправка в список somelist
somelist-admin отправка сообщения администраторам списка
somelist-bounces письма от неразрешенных адресов перенаправляются сюда
администратор также получит уведомление
somelist-confirm подтверждение при подписке или отписке
somelist-join запрос на подписку
somelist-leave запрос на отписку
somelist-owner отправка создателю списка
somelist-request изменение настроек подписки
somelist-subscribe синоним somelist-join
somelist-usubscribe синоним somelist-leave

Таким образом нам необходимо для каждого списка создать 10 email адресов в зоне mydomain.ru. А также 1 email noreply с которого будет происходить отправка писем с помощью модуля ASMTPDirect. Ещё 1 email администратора указывается при создании списка.

Создаем почтовый список somelist:
 ./bin/newlist somelist
GNU Mailman также требует создание списка mailman, иначе он не будет запускаться.
 ./bin/newlist mailman
Обязательно нужно указать email создателя списка и пароль для редактирования настроек списка через сайт.
Также потребуется создать 10 email адресов в вашем домене для списка somelist, с которых мы будем забирать почту через fetchmail. Т.к. список mailman не используется, создавать для него 10 email не обязательно.
Исходный список синонимов для перенаправления в mailman для локального SMTP сервера выглядит следующим образом:
somelist:              "|/opt/mailman/mailman/mail/mailman post somelist"
somelist-admin:        "|/opt/mailman/mailman/mail/mailman admin somelist"
somelist-bounces:      "|/opt/mailman/mailman/mail/mailman bounces somelist"
somelist-confirm:      "|/opt/mailman/mailman/mail/mailman confirm somelist"
somelist-join:         "|/opt/mailman/mailman/mail/mailman join somelist"
somelist-leave:        "|/opt/mailman/mailman/mail/mailman leave somelist"
somelist-owner:        "|/opt/mailman/mailman/mail/mailman owner somelist"
somelist-request:      "|/opt/mailman/mailman/mail/mailman request somelist"
somelist-subscribe:    "|/opt/mailman/mailman/mail/mailman subscribe somelist"
somelist-unsubscribe:  "|/opt/mailman/mailman/mail/mailman unsubscribe somelist"

В дальнейшем будем использовать эти синонимы как шаблон для настройки fetchmail.
Далее настроим получение почты с удаленного сервера с помощью fetchmail. Тут есть один нюанс: у пользователя от которого запускается fetchmail, основная (первая) группа должна быть mailman. Именно поэтому я решил использовать пользователя mailman для хранения конфига fetchmail.
Создаем конфиг .fetchmailrc
 touch /home/mailman/.fetchmailrc
 chmod 600 /home/mailman/.fetchmailrc
Примерная конфигурация будет выглядеть следующим образом:
poll pop.yandex.ru
port 995
protocol POP3
user "somelist@mydomain.ru" with password "pass" mda "/opt/mailman/mail/mailman post somelist"
ssl
sslcertpath /home/mailman/.sslcerts
sslfingerprint "44:A8:E9:2C:FB:A9:7E:6D:F9:DB:F3:62:B2:9E:F1:A9"

poll pop.yandex.ru
port 995
protocol POP3
user "somelist-admin@mydomain.ru" with password "pass" mda "/opt/mailman/mail/mailman admin somelist"
ssl
sslcertpath /home/mailman/.sslcerts
sslfingerprint "44:A8:E9:2C:FB:A9:7E:6D:F9:DB:F3:62:B2:9E:F1:A9"

...

И так далее для всех остальных email. Более подробно настройку сертификатов fetchmail смотри на OpenNET: http://www.opennet.ru/base/net/fetchmail_setup.txt.html

Запуск fetchmail от пользователя mailman будет выглядеть следующим образом:
 su mailman -c 'fetchmail -d 120 -a -L ~/.fetchmail.log 2>&1'
Остановка:
 su mailman -c 'fetchmail -q 2>&1'

Эти строки я рекомендую прописать в /etc/rc.d/rc.mailman для запуска и остановки вместе с mailman. Я проверяю почту каждые 120 секунд. Такого быстродействия вполне достаточно для слабоактивных списков рассылки.
Пропишем crontab для пользователя mailman:
 crontab /usr/local/mailman/cron/crontab.in -u mailman
И запустим mailman для обработки очередей входящих/исходящих писем:
 /etc/rc.d/rc.mailman start

Заключение

При желании можно настроить Mailman для работы с удаленным почтовым сервером. После всех настроек необходимо проверить работоспособность конфигурации.

Также был сделан SlackBuild который внутри уже содержит патч для создания модуля ASMTPDirect.py.

Ссылки:

Патч для ASMTPDirect.py: ASMTPDirect.patch

SlackBuild (проверялся на версии 2.1.18-1): mailman.tar.gz

четверг, 19 августа 2010 г.

Разделение отладочной информации и исполняемого файла в GDB

В GDB существует способ разделения отладочной информации и исполняемого файла.
Это актуально если программа не желает поставлять отладочную информацию вместе с бинарником.
Имеется возможность postmortem-отладки таких бинарников с последующей загрузкой креш-дампа и анализа как обычных с отладочной инфой.
Рассмотрим пример:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void bar()
{
   const char* crash_dir="/var/crash/";
   chdir(crash_dir);
   //По хорошему нужно написать функцию для определения имени краш-дампа
   //по анализу /proc/sys/kernel/core_pattern
   fprintf(stderr,"Created core dump %score.%d\n",crash_dir,getpid());
   abort();
}

void foo()
{
   int stack_variable=-1;
   bar();   
}

int main(int argc,char** argv)
{
   foo();
   return 0;
}

собираем наш пример:
gcc test.c -o test -g -O0

копируем отладочную информацию в отдельный файл test.debug:
objcopy --only-keep-debug test test.debug

удаляем отладочную информацию из основного файла:
strip -g test

добавляем ссылку на отладочную информацию в релиз файл:
objcopy --add-gnu-debuglink=test.debug test

В данном примере используется способ разделения отладочной информации с использованием gnu.debuglink. В результирующем файле будет ссылка на файл с отладочной информацией. Для того чтобы посмотреть что находится в секции файла .gnu_debuglink, можно воспользоваться коммандой:
objdump -s --section=.gnu_debuglink ./test
Мы увидим примерно следующее:
Contents of section .gnu_debuglink:
 0000 74657374 2e646562 75670008 e179394b  test.debug...y9K

запрашиваемая секция состоит из 3х частей:
1) имя файла с отладочной инфой к которому прилинкован бинарник - zero-terminated строка
2) затем выравнивание от 0 до 3х нулевых байт до следующих 4х байт
3) 4 байта crc от {полного содержимого файла с отладочной информацией}, которую gdb использует для проверки подргужаемого файла

Запустим наше приложение:
ulimit -c unlimited && ./test
Created core dump /var/crash/core.13708
Аварийный останов (core dumped)

С помощью команды ulimit мы указали размер допустимого креш дампа, приложение может также использовать setrlimit для указания данного лимита программно.

Проверка подргузки отладочных символов:
gdb --quiet --core=/var/crash/core.13708 ./test

Using host libthread_db library "/lib/i686/libthread_db.so.1".

Reading symbols from /lib/i686/libc.so.6...done.
Loaded symbols for /lib/i686/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./test'.
Program terminated with signal 6, Aborted.
#0  0xffffe410 in __kernel_vsyscall ()

При отладке с использованием gdb указавается обычный бинарник который получает пользователь, только gdb 'магическим' способом находит отладочную инофрмацию в прилинкованном файле. Проверим, как выглядит стек упавшей программы через bactrace full:
(gdb) backtrace full
#0  0xffffe410 in __kernel_vsyscall ()
No symbol table info available.
#1  0xb7e69c00 in raise () from /lib/i686/libc.so.6
No symbol table info available.
#2  0xb7e6b668 in abort () from /lib/i686/libc.so.6
No symbol table info available.
#3  0x08048477 in bar () at test.c:11
        crash_dir = 0x8048570 "/var/crash/"
#4  0x08048489 in foo () at test.c:17
        stack_variable = -1
#5  0x080484a1 in main () at test.c:22
No locals.
(gdb)
Мы видим все что было в стеке до падения. Правда libc у меня собрана без отладочной информации поэтому никаких переменных в стеке и информации о исходниках в пространстве этой библиотеки мы не увидим.

Имя файла с отладочной информацией может быть любым, но обычно имеет суффикс: .debug
gdb ищет файл с отладочной информацией сначала рядом с бинарником, затем в директории .debug
а затем внутри глобальной директории для отладки по пути с тем же именем из которого запущена программа.

Например, если есть бинарник /tmp/example/test то поиск бинарника с отладочной инфой test.debug будет произведен по путям:
/tmp/example/test.debug
/tmp/example/.debug/test.debug
/usr/lib/debug/tmp/example/test.debug где /usr/lib/debug глобальная директория для отладки

Для того чтобы узнать глобальную директорию для отладки нужно выполнить внутрях gdb:
show debug-file-directory
Для того чтобы сменить её используется команда set var_name = value.

Безусловно данный пример иллюстрирует возможности gdb с лучшей стороны, можно использовать данную технику для проприетарных приложений с закрытыми исходниками а также тестовых билдов где не критична сборка с оптимизацией. Можно отслеживать создание креш-дампов в обработчиках сигналов SIGSEGV или SIGBUS для того чтобы отловить падение программы, и отправить краш-дамп разработчикам. Единственная проблема это размер креш-дампов, но они хорошо жмутся с помощью gzip.

среда, 18 августа 2010 г.

Fun 'operator' в C++

Нашел интересную запись цикла:
#include <stdio.h>

int main(int argc,char** argv)
{
  int x = 10;
     while( x --> 0 ) // эквивалентно for(x=9;x>=0;x--)
     {
       printf("%d ", x);
     }
  x = 10;
     while( 0 <-- x ) // эквивалентно for(x=9;x>0;--x)
     {
       printf("%d ", x);
     }
  return 0;
}
Заметим что во втором случае обратная запись такого якобы 'оператора' --> не эквивалентна первой. В первом случае сначала происходит сравнение, потом декремент (постфиксная запись декремента). Во втором случае префиксный оператор декремента выполнится раньше чем оператор сравнения, и поэтому 0 мы не достигнем.

Взято с StackOverflow

среда, 11 августа 2010 г.

C++ Hiding Rule

Казалось бы, что не так в следующем коде:
#include <iostream>

class Foo {
public:
void Fn(){std::cout<<"Foo::Fn()"<<std::endl;};
};

class Bar:public Foo {
public:
void Fn(int i){std::cout<<"Bar::Fn()"<<std::endl;};
};

int main()
{
 Bar* b = new Bar;
 b->Fn(); 
 Foo* f = b;
 f->Fn(); 
 delete b;
}
GCC выдает:
In function 'int main()':
Line 17: error: no matching function for call to 'Bar::Fn()'
compilation terminated due to -Wfatal-errors.
На первый взгляд это кажется очень странным. Компилятор не может найти унаследованную функцию Bar::Fn. Попробуем ему помочь в этом:
class Bar:public Foo {
public:
using Foo::Fn;
void Fn(int i){std::cout<<"Bar::Fn()"<<std::endl;};
};

При таком раскладе все собирается нормально. Либо, можно убрать using, и в перегруженной функции явно вызвать функцию базового класса:
class Bar:public Foo {
public:
void Fn(){ Foo::Fn(); }
void Fn(int i){std::cout<<"Bar::Fn()"<<std::endl;};
};

Но почему так происходит? Оказывается здесь действует C++ Hiding Rule. При переопределении функций в классе-наследнике, все функции с таким же именем, но с другим прототипом унаследованные из базового класса становятся невидимыми в классе-наследнике. Это касается также и виртуальных функций.

Ссылка на FAQ

Интернационализация и локализация с использованием GNU GetText

Данный пост является небольшим обзором GNU GetText. Часто если требуется поддержка нескольких языков в программе, люди не знают с чего начать. Я решил написать небольшое введение, которое позволит устранить данную проблему.

Интернационализация (internationalization сокращённо i18n) - способность программы поддерживать несколько языков.
Локализация (localization сокращённо l10n) - способность программы автоматически подстраиваться под языковые и локальные настройки системы.

GNU GetText является частью glibc, поэтому если сборка приложения происходит в Linux (или другой системе, использующей glibc), то линковщику не требуется указывать никаких дополнительных библиотек. В свою очередь GNU GetText также использует libintl (GNU Internationalization runtime library) и libiconv. Перевод выполняется в процессе выполнения программы на лету. Для обеспечения этого, все строки которые должны быть переведены в программе, оборачиваются в вызов функции gettext().

Во время выполнения программы, для перевода GetText должен знать локализацию системы, для этого в Linux системах традиционно используются переменные окружения:
LC_ALL,LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC, LC_TIME,LANG которые означают категории локалей (т.е. соответствующие части)
Чтобы посмотреть состояние этих переменных воспользуйтесь командой `locale`

Для работы GetText наиболее значим LC_MESSAGES, именно эта категория отвечает за перевод сообщений.

Данные переменные окружения хранят имена локалей, которые базируются на трех основных терминах: код языка, код страны и кодировка. Имена локализаций строятся из этих частей следующим образом:

кодязыка_КОДСТРАНЫ.кодировка
Например коды языка и страны:

en_US    Английский - Соединенные Штаты
ru_RU    Русский для России
zh_TW    Традиционный китайский для Тайваня

В данных примерах отсутствует кодировка, также может отсутствовать и код страны. Т.е. например для русского языка в системе может быть несколько локалей, например: ru, ru_SU, ru_RU.KOI8-R, ru_RU.UTF-8 и т.д.

Расположение файлов перевода можно задать там где вам угодно, лишь бы их нашла программа, но традиционный путь в Linux: /usr/share/locale
Впрочем есть системы в которых путь к файлам локализации это /usr/local/share/locale или даже /usr/lib/locale, чтобы выяснить путь для вашей системы можно воспользоваться командой `locale -a -v`, вы увидите установленные локали и путь в котором они находятся.

При запуске программы в ней устанавливаются по умолчанию локаль С. Чтобы использовать другие локали, необходимо в программе использовать функцию setlocale. Имена категории локали совпадают с именами переменных окружения.

char *setlocale(int category, const char *locale);
Параметр category может быть один из списка:
LC_ALL  - используется для всей локали.
LC_COLLATE - для соотнесения регулярных выражений (определяет значение интервалов и классов эквивалентности) и сопоставления строк.
LC_CTYPE - для соотнесения регулярных выражений, классификации символов, преобразований, сравнения (с учетом регистра) и работы функций широких символов.
LC_MESSAGES - для сообщений, подлежащих локализации.
LC_MONETARY - для форматирования денежных единиц.
LC_NUMERIC - для форматирования чисел (используется десятичный разделитель, разделители тысяч и т.п.).
LC_TIME - для форматирования календаря и дат.

Параметр locale должен содержать имя предпочитаемой локали, например ru_RU.KOI8-R, либо быть NULL. Если locale равен NULL то, поиск локали происходит в переменных окружения. Порядок зависит от реализации. В случае с glibc сначала (независимо от category) проверяется переменная окружения LANGUAGE, затем LC_ALL, затем переменная окружения с таким же названием, что и категория (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC, LC_TIME), и в конце проверяется переменная окружения LANG. Используется первая существующая переменная окружения. Если ее значение не является правильным для локали, то локаль не изменяется, а setlocale возвращает NULL.
Успешный вызов setlocale() возвращает строку, соответствующую текущим установкам локали.

Чтобы gettext знал где находятся файлы перевода ему необходимо указать директорию с этими файлами, для этого используется функция bindtextdomain.

char * bindtextdomain (const char * domainname, const char * dirname);
Первым аргументом domainname должно быть имя файла локализации называемого в терминах GNU GetText домёном. Имя файла должно быть не NULL и без расширения. Оно чаще всего совпадает с именем пакета.

Вторым аргументом dirname должна быть директория в которой находятся иерархия файлов с локалями, или NULL чтобы узнать предыдущую заданную директорию.
На выходе в случае успеха возвращается установленная директория с локалями или NULL в случае неудачи.

Иерархия файлов внутри директории с локалями строится по следующей схеме:

locale_name/LC_category/domain_name.mo

locale_name - это имя локали, строящееся по тем же правилам что и в переменных окружения, т.е. по схеме кодязыка_КОДСТРАНЫ.кодировка. Причем как я упоминал выше, в имени локали может отсутсвовать код страны и кодировка. Применительно для GNU GetText LC_category всегда должен быть LC_MESSAGES, т.к. GetText ищет только директорию с переводимыми сообщениями. Имя домена это бинарный файл с переводимыми сообщениями.

Пример:
Пусть задана переменная окружения LANG=ru_RU.KOI8-R (locale_name)
Пусть файлы локализации хранятся в /usr/share/locale
Пусть задано имя приложения - gcc (domain_name)

Тогда поиск gcc.mo будет последовательно проходить по следующим путям:
/usr/share/locale/ru/LC_MESSAGES/gcc.mo
/usr/share/locale/ru_RU/LC_MESSAGES/gcc.mo
/usr/share/locale/ru_RU.KOI8-R/LC_MESSAGES/gcc.mo

Как видите вначале ищется языковая локаль, затем применительно к стране, и если не найдено в первых двух случаях, применительно к кодировке.
Как правило при создании дистрибутива как можно меньше стараются учитывать страну и тем более кодировку.

После того как известно имя локали и путь к локалям, приложению требуется имя файла в котором содержится перевод данного приложения (чаще всего совпадает исполняемым файлом приложения). Для указания конкретного файла переводов используется функция textdomain.

char * textdomain (const char * domainname);
Входным параметром этой функции является имя домена.

Выходным значением является заданный domainname или NULL в случае ошибки.

Более детальную справку по setlocale, bindtextdomain и textdomain можно посмотреть в соответствующем `man`.  

При работе с GetText-совместимыми библиотеками интернационализации приходится иметь дело со следующими типами файлов:

*.pot текстовый файл (Portable Object Template) содержит строки полученные сканированием исходников с помощью утилиты xgettext, и ещё не содержит переводов.
*.po текстовый файл (Portable Object) содержит перевод.
*.mo бинарный файл (Machine Object) т.е. это скомпилированный файл переводов, который загружается конечным приложением – в нем и хранятся переводы для конкретной локали, поиск происходит по индексу от исходной строки. 

Пример программы:

#include <locale.h>
#include <libintl.h>
#include <stdio.h>

#ifdef WIN32
#include <windows.h>
#endif

#define PACKAGE "example"
/* LOCALEDIR должен генерится системой сборки в зависимости от системы.
но для примера я использую директорию mo в той же директории что и приложение
т.е. в mo/ru/LC_MESSAGES
*/
#define LOCALEDIR "mo"

#define _(A) gettext(A)
#define N_(A) A
#define T_(A) _(A)
#define T_PLURAL(A,B,C) ngettext(A,B,C)

int main(int argc,char** argv)
{
/*  Установка всей локали в соответствии
с переменными окружения
*/
setlocale(LC_ALL,"");
/*  Установка пути к локалям
*/
bindtextdomain(PACKAGE,LOCALEDIR);
/*  Установка имени файла перевода (домёна)
*/
textdomain(PACKAGE);
#ifdef WIN32
/*  Для Windows укажем с помощью bind_textdomain_codeset
выходную кодировку переводов такую же как кодировка на консоли.
В Linux у меня никогда таких проблем не было.
Там версия gettext всегда улавливала кодировку терминала как минимум
потому что в LANG у меня всегда указывается кодировка.
*/
char windows_codepage[10];
snprintf(windows_codepage,10,"CP%d",GetConsoleCP());
bind_textdomain_codeset(PACKAGE,windows_codepage);
#endif
/* Отображаем 2 раза строку для перевода,
заметим что в pot попадет только одна строка.
*/
printf("%s\n",T_("This string will translate"));
printf("%s\n",T_("This string will translate"));
/* Пример использования форм множественного числа.
*/
int n;
for(n=1;n<10;++n)
/*TRANS: Данный комментарий видим переводчику.
Переводчик должен создать корректные формы множественного числа: */
printf(T_PLURAL("Give me %d apple\n","Give me %d apples\n",n),n);
return 0;
}
Если не найден перевод, функция gettext возвращает исходную строку, иначе переведенную. Также мы используем функцию ngettext для перевода строк в которых есть слова, окончания которых должны меняться в зависимости от некоторого количества. (1 Штука, 2 Штуки, 5 Штук).
Тут хочется отметить что обычно никто gettext напрямую не вызывает из соображений размера исходников. Для этого делаются подобные макросы:

#define _(A) gettext(A)
#define N_(A) A
#define T_(A) _(A)

Макрос N_ нужен для того чтобы создавать переменные которые позже будут переведены c помощью gettext. Например:

const char* str=N_("This string will translate");
printf("%s\n",_(str));

Сам GetText предлагает gettext_noop макрос аналогичный N_ (т.е. не навязывает вас делать свои макросы).  

Создание перевода происходит в 3 этапа:
1) Парсинг исходных кодов программы с помощью утилиты xgettext и создание шаблона для перевода.
2) Копирование *.pot файла в рабочий *.po файл и перевод *.po файла.
3) Создание *.mo файла с помощью утилиты msgfmt, и его дальнейшее расположение в необходимой директории.

Перейдем в каталог с нашим примером и получим шаблон строк для перевода:

xgettext --default-domain=example --from-code=UTF-8 \
--add-comments=TRANS: \
--keyword=_ --keyword=T_ --keyword=N_ --keyword=T_PLURAL:1,2 \
-o example.pot example.c
В случае нахождения одинаковы строк для перевода, xgettext не будет повторять эту строку несколько раз в файле, а просто добавит ещё информацию о позиции в файле откуда вытаскивается строка. --add-coments=TRANS: позволяет добавить комментарии переводчику, начинающиеся с TRANS. Опции keyword говорят о том по каким ключевым словам вытаскивать фразы из исходника.

Создадим файл рабочих переводов из шаблона:

msginit --input=example.pot --no-translator --locale=ru_RU --output-file=example.po
По сути данная операция просто копирует pot файл в po. Но я не пользую cp, потому что msginit за меня может заполнить информацию о переводчике, выставить корректно кодировку, а также указать правило для выборки форм множественного числа (plural-forms) в зависимости от указанной локали. Я использую --no-translator, потому что у меня не задана информация о email в моей системе, потом я правлю po файл руками. Заметим что в реальных приложениях эта операция делается один раз, и дальше существующий po файл растет по мере роста pot файла. Для обновления уже существующего po файла с переводами необходимо воспользоваться командой: `msgmerge existing.po new.pot –o existing.po`

Дальше необходимо заполнить наш po файл:

# Russian translations for PACKAGE package
# Copyright (C) 2010 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Automatically generated, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-08-11 02:24+0700\n"
"PO-Revision-Date: 2010-08-11 01:21+0700\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

#: src/example.c:43 src/example.c:44
msgid "This string will translate"
msgstr "Эта строка для перевода"

#. TRANS: This comment is visible for translator
#. translator must create correct plural forms:
#: src/example.c:50
#, c-format
msgid "Give me %d apple\n"
msgid_plural "Give me %d apples\n"
msgstr[0] "Дай мне %d яблоко\n"
msgstr[1] "Дай мне %d яблока\n"
msgstr[2] "Дай мне %d яблок\n"
В файле переводов первым идет MIME заголовок. В нем поля говорят сами за себя, там должны находится сведенья о переводчике и команде переводчиков. Пристальное внимание нужно уделить charset. Кодировка файла должна совпадать с этим полем.  

Рассмотрим формат переводимых строк:

white-space
#  translator-comments
#. automatic-comments
#: reference...
#, flag...
msgid untranslated-string
msgstr translated-string
Все что помечено # является необязательным комментарием, и не используется при создании mo файлов. Комментарии которые после # имеют пробел создаются только переводчиком, остальные как правило с помощью различных утилит. Автоматические комментарии это комментарии от разработчиков. reference создается с помощью xgettext, в нем указывается имя исходника и номер строки из которой получена данная фраза. Что касается флагов после #, то единственный флаг на который стоит обратить внимание это #, fuzzy – пометка о том что данный перевод может быть не точным. Данный флаг может быть выставлен как переводчиком так и утилитой msgmerge при слиянии po и pot файлов.

Ну и наконец самое главное это msgid – строка, которая вытащена из исходников и msgstr – это её перевод.

Стоит обратить внимание на Plural-Forms, данное правило добавлено msginit в соответсвии с указанной локалью ru_RU. Дело в том, что для большинства языков, число форм единственного и множественного числа является конечным. Для английского существует всего 2 формы - это единственное и множественное число, которое образуется добавлением –s или –es. Поэтому мы видим msgid форму единственного числа, msgid_plural – форму множественного числа. И этого достаточно, если учесть что все переводимые фразы в программе на английском языке. А вот перевод уже может содержать больше чем 2 формы. Поэтому msgstr заполняется как массив. Количество форм окончаний для русского языка – 3. Правило для выборки конкретной формы указывается в Plural-Forms. Хочу напомнить что его не нужно сочинять, msginit корректно его заполняет в зависимости от указанной локали.

Естественно вручную *.po файлы переводчикам править не нужно ;). Я рекомендую PoEdit: http://www.poedit.net/

Создадим *.mo файл для нашего *.po файла:

msgfmt -o example.mo example.po
Дальше наш mo файл необходимо положить в mo/ru/LC_MESSAGES.

Собранное приложение example при запуске из той же директории, где находится директория mo - должно в зависимости от переменных окружения менять локализацию. Желающие могут поиграться с переменными окружения поменять их на ru или en и посмотреть как ведет себя приложение.

Архив с исходниками находится тут. Под linux как правило все библиотеки уже установлены. Для Windows, я использовал MinGW и для него придется скачать пакет GetText слинковать приложение с libintl. Можно просто указать соответствующие флаги при вызове make: mingw32-make "LDFLAGS += -lintl".

GetText поддерживается в Windows как портированное приложение, возникает вопрос что делать, чтобы не задавать локаль в переменных окружения? (Для оконных приложений это критично). Если покопаться в исходниках wxWidgets (данная библиотека совместима с GetText) можно увидеть следующий код:

LCID lcid = GetUserDefaultLCID(); //узнаем идентификатор локали
WORD langID = LANGIDFROMLCID(lcid); /*получаем идентификатор языка  
                                      из идентификатора локали (младшие 2 байта)*/
uint32 lang = PRIMARYLANGID(langID);//получаем идентификатор основного языка
uint32 sublang = SUBLANGID(langID); //получаем идентификатор региона/страны
По полученным идентификаторам составляется таблица в соответствии с MSDN для поиска по идентификатору имени страны и языка, которые можно подсунуть в setlocale,bindtextdomain.

Если вам понравилось работать с GetText-подобными системами перевода приложений то можете попробовать себя в переводе свободных приложений ;). Для этого вам нужно посетить http://www-ru.gnu.net.ru/start.html и http://translationproject.org/team/ru.html
Хочется особо отметить, что для того чтобы ваш перевод попал в конечный дистрибутив вы должны заполнить и отправить Disclaimer на ваши переводы в FSF: http://translationproject.org/disclaim.txt

Ссылки:

http://www.gnu.org/software/gettext/
http://www.gnu.org/software/gettext/manual/gettext.html
http://ru.wikipedia.org/wiki/Gettext