baseciq.org

Exim i MySQL

Uwaga! Tekst ten proszę traktować jako swoisty proof-of-concept i proszę, nie opierajcie na nim systemów produkcyjnych. Jeżeli zbudujecie na podstawie mojego opisu dokładnie go przetestujecie. Generalnie rozwiązania podane poniżej u mnie działają, ale nie gwarantuje że jest to wszystko bezpieczne i bezbłędne. Jeżeli tekst mój przyda się w budowie porządnego systemu zarządzania kontami mail, mam nadzieję że zostanie on udostępniony na licencji GPL.

Jestem absolutnie świadom że poniżej opisane rzeczy można zrobić dwadzieścia razy lepiej, estetyczniej, sprawniej, bezpieczniej i w ogóle cudownie. To ma być jedynie metoda na nauczenie kogoś jak to się robi!

Masz 300 użytkowników którzy korzystają tylko z poczty i sądzisz że nie potrzebują oni dostępu do shella? A może uważasz za upierdliwe logowanie się na ssh i zmiana parametrów konta? A może poprostu jesteś leniwy i chciałbyś aby to wsio było kilkane? :) Oki. Postaramy się coś z tym zrobić.

Główną ideą rozwiązania które będę wam tłumaczył jest to, żeby konta tylko pocztowe były kontami wyłącznie pocztowymi. Po co? Takich kont może być maksymalnie 65*1024. Dla większości jest to liczba wręcz przerażająco wielka. Dla średniej wielkości ISP jest to liczba raczej mała. Osobiście nie potrzebowałem obsługi nawet 100 kont, ale ilość kont to nie jedyna zaleta. Oparcie konfiguracji na bazie danych MySQL pozwola na łatwą integracje z innymi usługami opartymi na tej bazie danych, a co najważniejsze, łatwe tworzenie aplikacji internetowych w PHP do zarządzania tym systemem.

Założenia

- osoba która to instaluje ma mózg i go używa do myślenia a nie jako grzechotkę;
- posiadasz jakąś wiedzę o MySQL, umiesz tworzyć zapytania, wiesz czym się różni kolumna od tabeli i tabela od bazy;
- umiesz skompilować i zainstalować exima z MySQL'em - ten tekst napewno tych czynności nie opisze;
- Exim 4.x (testowane na 4.12 zbudowanym z CVS'u PLD)

Tak więc masz już Exima i MySQL'a odpalone. Zacznijmy od podstaw - czyli skonfigurowania połączenia do bazy. W głównej sekcji configa (np. na samej górze) dopisujemy:

hide mysql_servers = "localhost/poczta/mysql/haslo"

Gdzie localhost to każdy chyba wie co to znaczy, poczta to nazwa bazy danych, mysql to konto użytkownika MySQL a haslo to hasło ;) Zacznijmy od podstaw, czyli listy domen jakie ma nasz exim obsługiwać jako lokalne. Utwórzmy prostą tabelę zawierającą jedną kolumnę z nazwą domeny:

CREATE TABLE domeny (nazwa VARCHAR(64) NOT NULL);

Każda domena wpisana do tej tabeli będzie traktowana przez exima jako domena lokalna. Aby taką zachciankę wprowadzić w życie nauczmy exima korzystać z tej tabeli. Za listę domen które exim ma traktować jako lokalne służy opcja domainlist local_domains:

domainlist local_domains = @ : ${lookup mysql {SELECT nazwa FROM domeny WHERE nazwa="${domain}"}}

Znak @ oznacza w tej liście podstawowy hostname maszyny - czyli jeżeli nasza maszyna nazywa się "pepsi.netx.waw.pl" to poczta dochodząca do domeny pepsi.netx.waw.pl będzie przez exima traktowana jako lokalna. Jeżeli już Cię zżera ciekawość czy to działa, dodaj do mysql'a jakąś domenę:

INSERT INTO domeny (nazwa) VALUES ('dupa.pl');

Po takim zabiegu można odrazu spróbować wysłać pocztę na jakieś konto na naszym serwerku w domenie dupa.pl :) Tak więc domeny już mamy. Teraz trzeba jakoś zrobić skrzynki pocztowe. Stwórzmy zatem tabelę trzymającą dane o kontach:

CREATE TABLE skrzynki (nazwa VARCHAR(32) NOT NULL, domena VARCHAR(64) NOT NULL);

Następnym krokiem to znowu nauczenie exima korzystania z tej tabeli. W sekcji ROUTERS CONFIGURATION (czyli po 'begin routers') dopisujemy nowy 'router'. Ważna tutaj jest kolejność wpisów. Dopisanie nowego routera przed standardowymi 'system_aliases', 'userforward' i 'localuser' spowoduje to, iż exim najpierw będzie sprawdzał czy istnieje użytkownik w bazie danych i jeżeli nie dopiero po tym czy istnieje 'prawdziwe' takie konto. Tak więc tworzymy nowy router, najlepiej zaraz po routerze dnslookup:

mysql_localuser:
	driver = accept
	domains = +local_domains
	condition = ${if eq{}{${lookup mysql {SELECT nazwa FROM skrzynki WHERE \
		nazwa='${local_part}' AND domena='${domain}'}}}{no}{yes}}
	transport = mysql_delivery
	no_more

Jako transport (czyli określenie co odpowiada za dostarczenie poczty) określiliśmy mysql_delivery które zaraz również utworzymy. Istotną wadą powyższego wpisu jest także to, że będzie on także obejmował domeny 'prawdziwe', tj. te które nie są w bazie, co może nie każdemu adminowi odpowiadać. Aby to poprawić wracamy się do domainlist local_domains i tworzymy dodatkową listę domen:

domainlist mysql_local_domains = ${lookup mysql {SELECT nazwa FROM domeny WHERE nazwa="${domain}"}}

Teraz, w mysql_localuser zmieniamy tylko domains = +local_domains na domains = +mysql_local_domains. Kolejnym krokiem jest zdefiniowanie transportera (można to wytłumaczyć jako 'urządzenie dostarczające pocztę'). Przed dopisaniem transportera, musimy zdecydować gdzie chcemy trzymać pocztę - czy w jakimś konkretnym katalogu, np. '/var/mail/virtual/nazwa_domeny/nazwa_skrzynki, czy np. chcemy katalog określić w bazie danych. Ja ograniczę się narazie do pierwszego rozwiązania. Odnajdujemy begin transports i dopisujemy:

mysql_delivery:
	driver = appendfile
	file = /var/mail/virtual/${domain}/${local_part}
	delivery_date_add
	envelope_to_add
	return_path_add

Jak widzicie w tej sekcji nie ma żadnego zapytania MySQL. Dlaczego? Otórz mysql_local_domains które zdefiniowaliśmy wcześniej sprawdza czy konto w domenie i domena istnieją. Tak więc jeżeli chcemy dostarczać pocztę do normalnych skrzynek pocztowych w formacie BSD (czyli takich jakie są w /var/spool/mail) tylko że w podkatalogach mających taką samą nazwę jak domena to nie potrzeba nic z MySQL'a pobierać. Tak więc zapisujemy zmodyfikowany plik konfiguracyjny, restartujemy exima i sprawdzamy co nam z tego wyszło :) Stwórzmy w MySQL'u domenę i konto w niej:

INSERT INTO domeny (nazwa) VALUES ('poczta.test');
INSERT INTO skrzynki (nazwa, domena) VALUES ('testowekonto','poczta.test');

Wysyłamy maila:

$ mail testowekonto@poczta.test -s "wiadomosc testowa" < /dev/null
Null message body; hope that's ok

Teraz po zajrzeniu do katalogu /var/mail/virtual/poczta.test ujrzymy pliczek testowekonto :) Oczywiście jest to bardzo proste dostarczanie. Nie ma na przykład jeszcze aliasów. Tak więc dodajmy kolumnę 'alias' do tabeli z userami. Zasada działania będzie taka, że jeżeli przy jakimś koncie w tej tabeli zostanie dopisany alias, to exim przekieruje pocztę na zawartość kolumny alias. Zmodyfikujmy więc tabelę dodając do niej kolejną kolumnę:

ALTER TABLE skrzynki ADD alias VARCHAR(64) DEFAULT '' NOT NULL;

Aby exim umiał korzystać z tych aliasów, należy zmodyfikować router mysql_localuser aby nie dostarczał poczty jeżeli istnieje coś w kolumnie alias (prosta zmiana w kwerendzie sql'a) oraz żeby dostarczał aliasy. Modyfikujemy więc mysql_localuser oraz dopisujemy przed nim router mysql_alias:

mysql_alias:
	driver = redirect
	domains = +mysql_local_domains
	allow_fail
	allow_defer
	data = ${lookup mysql {SELECT alias FROM skrzynki WHERE nazwa='${local_part}' \
	AND domena='${domain}' AND alias!='' AND alias!=CONCAT('${local_part}@','${domain}')}}

mysql_localuser:
	driver = accept
	domains = +mysql_local_domains
	condition = ${if eq{}{${lookup mysql {SELECT nazwa FROM skrzynki WHERE \
	nazwa='${local_part}' AND domena='${domain}' AND alias=''}}}{no}{yes}}
	transport = mysql_delivery
	no_more

Kwestii wyjaśnienia - te kombinacje z CONCATem mają na celu zapobiegnięcie sytuacji gdy konto 'cos' w domenie 'domena.pl' będzie wskazywało przez alias na 'cos@domena.pl' :) Przetestujmy to 'udogodnienie', dodając aliasa do bazy:

INSERT INTO skrzynki (nazwa, domena, alias) VALUES ('testalias','poczta.test','testowekonto@poczta.test');

Spróbujmy teraz wysłać maila pod adres testalias@poczta.test. Warto zauważyć, że exim bezbłędnie sobie poradzi, jeżeli komplet nazwa+domena będzie występował podwójnie wskazując na dwa różne aliasy - poprostu dostarczy maila do dwóch aliasów. Jeżeli natomiast będą istnieć aliasy dla jakiegoś konta, oraz będzie normalne konto bez aliasu, to poczta zostanie dostarczona tylko na aliasy, podobnie jak normalne aliasy np. w takim sendmailu. Kolejnym 'feature' będą aliasy domenowe. Co to ma być? Poprostu np. wysyłamy maila na adres konto@poczta.test, a adres jest przepisywany jako konto@innadomena. Ta 'innadomena' może być równie dobrze domeną lokalną, wirtualną, bądź domeną na zupełnie innym serwerze. Zmodyfikujmy więc tabelę z domenami:

ALTER TABLE domeny ADD alias VARCHAR(64) DEFAULT '' NOT NULL;

Zmodyfikujmy mysql_local_domains tak aby wskazywał na domeny które nie są aliasami, oraz utwórzmy listę mysql_alias_domain:

domainlist mysql_local_domains = ${lookup mysql {SELECT nazwa FROM domeny WHERE nazwa="${domain}" AND alias=''}}
domainlist mysql_alias_domains = ${lookup mysql {SELECT nazwa FROM domeny WHERE nazwa="${domain}" AND alias!=''}}

Dodajmy przed routerem mysql_alias kolejny router, mysql_domainalias:

mysql_domain_alias:
	driver = redirect
	domains = +mysql_alias_domains
	allow_fail
	allow_defer
	data = ${lookup mysql {SELECT CONCAT('${local_part}@', alias) FROM domeny WHERE \
	nazwa='${domain}' AND alias!=''}}

Teraz dodajmy aliasa domenowego:

INSERT INTO domeny (nazwa, alias) VALUE ('poczta.alias', 'poczta.test');

Teraz poczta wysłana na testowekonto@poczta.alias spowoduje dostarczenie maila do testowekonto@poczta.test.

Uf. Więc teoretycznie mamy już exim'a skonfigurowanego. Teraz pobawimy się, potestujemy i przejdziemy do konfiguracji serwera POP3, bo przecież samo dostarczanie to chyba nie do końca pełny system.

Serwer POP3

Mój wybór padł na tpop3d - całkiem miły i wydajny serwerek pop3. Podobnie jak i w przypadku exim'a, daruje sobie konfigurację i kompilacje (zresztą, w PLD jest dobry i sprawny ten tpop3d :)). Zacznijmy jednak od tego, by wzbogacić naszą bazę danych o hasła:

ALTER TABLE skrzynki ADD haslo VARCHAR(128) DEFAULT '' NOT NULL;

Trzeba oczywiście skonfigurować demon pop3d w /etc/tpop3d.conf:

# Słuchaj na każdym adresie IP:

listen-address: 0.0.0.0
max-children: 10
timeout-seconds: 30

# Domyślnie skrzynka jest w /var/mail/konto:

mailbox: /var/mail/$(user)

# Włączmy możliwość zalogowania się via PAM, czyli dla normalnych
# lokalnych kont:

auth-pam-enable: yes
auth-pam-facility: tpop3d
auth-pam-mail-group: mail

# Autoryzacja użytkowników z MySQL'a:

auth-mysql-enable: yes
auth-mysql-mail-group: mail
auth-mysql-hostname: localhost
auth-mysql-database: poczta
auth-mysql-username: mysql
auth-mysql-password: haslo
auth-mysql-pass-query: SELECT CONCAT('/var/mail/virtual/','$(domain)','/', '$(local_part)'), \
	CONCAT('{plaintext}', haslo), 'exim', 'bsd' FROM skrzynki WHERE \
	nazwa = '$(local_part)' AND domena = '$(domain)' AND alias=''

Jeżeli interesuje Cię support do SSL w tpop3d - zapraszam tutaj.

Ustawmy dla testu hasło:

UPDATE skrzynki SET haslo='haslodlatestu' WHERE nazwa='testowekonto' AND domena='poczta.test';

Teraz spróbujcie odebrać pocztę, używając użytkownika: testowekonto@poczta.test i hasła: haslodlatestu. Oczywiście w bazie możemy używać haseł md5 (takich jak shadow) wpisując w auth-mysql-pass-query zamiast {plaintext} słowo {crypt_md5} (takie hasło można uzyskać przy pomocy komendy crypt() z PHP na każdym w miarę nowym linuksie). Przykładowe takie hasło wygląda np. tak: $1$2sJSEoGq$rKMII73MsDYbcEd.cU55f..

Duplikowanie wpisów

Dosyć poważnym problemem którego do tej pory nie poruszyłem są zduplikowane wpisy w bazie danych. Od tego służą klucze w MySQL. Dla tabeli ze skrzynkami, klucz powinien obejmować komplet nazwa-domena-alias. Dlaczego? Pozowli to na utworzenie konta które jest aliasem i prowadzi na kilka innych kont. Niestety pozwoli to na jednoczesne utworzenie kilku aliasów oraz normalnego konta które wtedy nie będzie działać. Pozwalam sobie na taką sytuację gdyż w normalnych systemach pocztowych taka sytuacja też jest możliwa (istnieje konto w systemie, jednakże aliasy przechwytują poczte dla niego, np. konto root'a). W przypadku domen, klucz powiniene obejmować samą kolumnę 'nazwa'. Oto więc komplety zrzut tabel z naszej bazy danych którą można wykorzystać w konfiguracji opisanej powyżej:

CREATE TABLE domeny (
  nazwa varchar(64) NOT NULL default '',
  alias varchar(64) NOT NULL default '',
  PRIMARY KEY  (nazwa)
) TYPE=MyISAM;

CREATE TABLE skrzynki (
  nazwa varchar(32) NOT NULL default '',
  domena varchar(64) NOT NULL default '',
  alias varchar(64) NOT NULL default '',
  haslo varchar(128) NOT NULL default '',
  PRIMARY KEY  (nazwa,domena,alias)
) TYPE=MyISAM;

Mam nadzieję że pozwoli to wam zbudować własny system poczty wirtualnej.