Hlavní navigace

Sendmail: různá práva různým uživatelům

13. 12. 2006
Doba čtení: 12 minut

Sdílet

Řešení neobvyklého problému se Sendmailem. Cílem je vytvořit u zákazníka dva typy uživatelského účtu. Jedni uživatelé mohou komunikovat e-mailem jen uvnitř firmy, ostatní pak mohou poštu odesílat i přijímat zvenčí. Jak tento problém v Sendmailu vyřešit? A proč zvolit právě Sendmail?

Požadavek zákazníka byl, aby někteří uživatelé mohli mailovat jen v rámci lokální sítě a někteří aby komunikovali normálně. Trošku upřesním ty dvě skupiny:

normální – běžný uživatel emailu, může posílat kamkoliv a přijímat odkudkoliv

omezený – pokud se jedná např. o doménu firma.cz, omezený uživatel nemůže poslat email jinam než na adresy neco@firma.cz

Stejně tak i s příchozí poštou, přijme email jen od ostatních uživatelů v doméně firma.cz. Samozřejmě chceme, aby byl uživatel informován o nedoručitelné nebo neodeslatelné zprávě.

U zákazníka existovalo ne zcela vyhovující řešení pomocí dvou účtů v Outlook expressu – uzivatel@firma­.local a uzivatel@firma.cz. Několik nevýhod – dva účty na pc, uživatelé si často zapomenou vybrat správného odesílatele a maily se jim vrací jako nedoručitelné (posílají např. z firma.local na nekdo@jinafir­ma.cz), … Uživatelsky příjemnější řešení by bylo, kdyby pro každého uživatele existoval jen jeden účet a sendmail sám by řešil povolené uživatele a směry pošty. Jen v případě problému by upozornil uživatele (nedoručitelnost, obecná adresa, …).

Proč sendmail? Má jednoduchou základní konfiguraci, je na drtivé většině linux/unix distribucí a hlavně je to velmi mocný nástroj. Daný problém jde řešit jednoduchou úpravou konfiguračního souboru.

Jak se sendmail konfiguruje?

Říká se: Člověk není opravdovým správcem systému, dokud nezkusil ručně editovat sendmail.cf. Člověk je blázen, pokud to zkusil víc nez jednou. Když se podíváte na soubor sendmail.cf (konfigurační soubor sendmailu), zjistíte, že je to asi pravda. Je poměrně velký (desítky kilobajtů) a pro běžného člověka není moc čitelný.

Malá ukázka .cf:

######################################################################
###  try_tls: try to use STARTTLS?
###     (done in client)
######################################################################
Stry_tls
R$*             $: $>D <$&{server_name}> <?> <! "Try_TLS"> <>
R<?>$*          $: $>A <$&{server_addr}> <?> <! "Try_TLS"> <>
R<?>$*          $: <$(access "Try_TLS": $: ? $)>
R<?>$*          $@ OK
R<$* <TMPF>>$*  $#error $@ 4.3.0 $: "451 Temporary system failure. Please try again later."
R<NO>$*         $#error $@ 5.7.1 $: "550 do not try TLS with " $&{server_name} " ["$&{server_addr}"]"

Na pomoc máme ale makrojazyk M4. Díky němu je konfigurace o dost jednodušší. Tento konfigurační soubor má typicky příponu .mc. V něm jsou zapsána makra, která popisují chování sendmailu. Nám stačí znát jen jejich názvy a parametry. Na běžné doručování zpráv si vystačíte asi s 20 makry. Pomocí maker a šablony je pak vygenerován odpovídající .cf soubor.

.mc už vypadá lépe:

OSTYPE(freebsd5)
DOMAIN(generic)

FEATURE(access_db, `hash -o -T<TMPF> /etc/mail/access')
FEATURE(blacklist_recipients)
FEATURE(local_lmtp)
FEATURE(mailertable, `hash -o /etc/mail/mailertable')
FEATURE(virtusertable, `hash -o /etc/mail/virtusertable')

Samozřejmě nejde makry pokrýt veškeré detaily chování, ale pro běžný provoz bohatě dostačují. Nebudu strašit a rovnou řeknu, že souboru .cf se nedotkneme. Veškeré úpravy budou v .mc. Je to i výhodnější z hlediska práce. Když bych například udělal změnu v .mc, vygeneroval .cf a ještě tam musel doplnit svou úpravu, bylo by to pracné.

V .mc souboru se múžete setkat s několika formáty poznámek. Vynechám divert, v našem případě to budou značky # a dnl.
dnl – vše za dnl až do konce řádku se považuje za poznámku a nepřenáší se do .cf souboru.
# – je též poznámka, ale opíše se do .cf souboru.

Běžná makra a zápisy jako FEATURE, DOMAIN, define teď přeskočíme. Soubor .mc je připraven většinou dobře, stačí pár drobných úprav a vygenerovat z něho .cf a fungujeme. Pro naši úpravu ale potřebujeme něco víc, a to sady rulesetů a vlastní pravidla.

Jak sendmail pracuje?

Pravidla

Sendmail využívá tzv. sady přepisovacích pravidel. Hlavičky každého procházejícího mailu jsou porovnány s těmito pravidly a na základě jejich výsledku se např. modifikuje daná hlavička, určuje se další zpracování celého mailu, … Pravidlo má dvě části – pravou a levou. Když hlavička splní levou podmínku, vykoná se pravá strana pravidla. Levá a pravá část MUSÍ být oddělena alespoň jedním tabulátorem. Pravidlo může mít ještě třetí (nepovinnou) část, a tou je poznámka. Opět je oddělena tabulátorem. Levá strana je vlastně podmínka – vzor, se kterým se řetězec porovnává. Může obsahovat písmena, znaky a existuje několik speciálních sekvencí, např. $+, $-, $@. Co znamenají? Hlavička se rozděluje na řetězce, kterým se říká tokeny. Jsou oddělené mezerami. Žádný token nemůže obsahovat mezery. Speciální sekvence nám vlastně popisují tokeny:

$@ vyhovuje právě žádný token
$* vyhovuje žádný nebo více tokenů
$+ vyhovuje jeden nebo více tokenů
$- vyhovuje právě jeden token

Sekvencí je víc, ale pro náš příklad stačí to, co je v této tabulce.

Na pravé straně může být také několik sekvenci se speciálním významem, my využijeme jen některé:

$n nahradí se n-tým výrazem z levé strany
$#mailer ukončí vyhodnocování sady pravidel, určuje mailer, který se použije k doručení zprávy
$@ Zbytek pravé strany bude předán jako výstup celé sady. Další pravidla v sadě se nezpracovávají

když je levá strana splněna, přichází na řadu pravá strana. Smaže se porovnávaná hlavička a místo ní se použije právě tato pravá strana. Důležitá věc – tokeny začínajících znakem $ na levé straně se zkopírují do proměnných ($1, $2,…) a můžeme s nimi pracovat na pravé straně. Další sekvence jsou uvedené v tabulce 2. Ukážeme si to na konkrétním příkladu, vstupní řetězec je „jan novak“ <novak@firma.cz>:

R$*                     $: <oo> < $1 >

R na začátku určuje, že se jedná o přepisovací pravidlo (je spousta dalších identifikátorů: M – definice maileru, D – definice makra,…). Tomuto pravidlu vyhoví žádný nebo více tokenů, takže např. i námi zadaná adresa. Protože je podmínka splněna, pravá strana nám vytvoří nový řetězec začinající značkou (flagem) <oo> a opsáním $1. Výsledkem je tedy <oo> „jan novak“ < novak@firma.cz >

R<oo>$+ < $* >          $: <oo> < $2 >

touto podmínkou si vlastně rozdělíme zadanou hlavičku na části „jan novak“ a < novak@firma.cz >. Pravá strana nám z toho udělá <oo> < novak@firma.cz >. V podstate jsme odřízli zbytečný text z hlavičky z levé strany. Emailovou adresu jsme zachovali.

R<oo>< $* > $+          $: <oo> < $1 >

To samé jsme udělali i s pravou stranou. V našem případě tato podmínka nebyla splněna, takže pravá strana se neuplatní a máme stále <oo> <novak@firma.cz>. A proto se uplatní další pravidlo. Pravá strana nás pak zbaví značky <oo>.

R<oo>< $+ >             $: $1

Nechci zde pravidla dopodrobna rozebírat. Jen poslední dvě zmínky:
- proč je tam značka (flag) <oo>? Být tam nemusí, ale značně nám zpřehledňuje kód, umožňuje nám „přeskakovat“ pravidla (uvidíte v praktickém příkladu, hledejte <OK>) a můžeme si tak značit, v jaké fázi jsme.
- proč používáme < a >? Též se nemusí používat, ale zpřesňují nám zápis pravidel a umožňují nám lépe pracovat s tokeny. Např. pokud bych měl zápis flag adresa rcpt_host, těžko bych rozpoznával jednotlivé části. Flag (<oo>) ještě poznáme ($-), ale protože nevíme, kolik tokenů obsahuje adresa a kolik rcpt_host, jsme v koncích. Pokud si to ale zapíšeme jako flag < adresa > < rcpt_host >, rozlišit od sebe jednotlivé části už není problém: $- < $* > < $* >. A díky tomu můžeme na pravé straně používat $1 pro flag, $2 pro adresu a $3 pro rcpt_host.

Sady

Pravidla jsou v sdružena v tzv. sadách pravidel. Každá sada se volá při jiné fázi zpracování pošty.

Ruleset 3 – převádí adresu v libovolném formátu do běžného formátu, který bude následně dál zpracován
Ruleset 2 – většinou se nepoužívá, uplatní se na všechny adresy příjemců
Ruleset 1 – jako 2, jen na odesílatele
Ruleset 0 – aplikuje se na adresy příjemce po sadě 3, měla by vygenerovat příkaz, který doručí zprávu příjemci. Ten obsahuje 3 věci: mailer, host a uživatel

My máme k dispozici m4 makra LOCAL_RULE3, LOCAL_RULE2, LOCAL_RULE1 a LOCAL_RULE0. Ty nám umožňují rozšířit příslušné rulesety o další pravidla.

Pro naši úpravu využijeme maker LOCAL_CONFIG a LOCAL_RULESETS. Zapisují se do .mc souboru. LOCAL_CONFIG nám umožní přidat do .cf další informace a pravidla (v našem případě definovat seznam omezovaných uživatelů). Ty jsou načteny a vykonány po běžných makrech, ale před standardními rulesety.

Dále existují rulesety check_mail, check_rcpt a check_relay. Ty slouží k testování údajů předaných z hlaviček MAIL FROM:, RCPT_TO: a adresy hosta. Pomocí LOCAL_RULESET je můžeme rozšířit o naše pravidla (název většinou Local_…). My vytvoříme ruleset s názvem Local_check_rcpt. Ruleset Check_rcpt dostává celou adresu z hlavičky RCPT TO: a volá Local_check_rcpt (pomocí pomocí $>). Pěkně je to vidět v sendmail.cf.

U local_check_* máme několik možností:

pokud na pravé straně pravidla uvedeme $#error nebo $#discard, akce se provede (jeden z nášich případů) a tím se ukončí další testování. Pokud $#OK, další testování se neprovádí (ani ruleset Check_*). Pokud ani jedna tato věc není splněna, pokračuje se dál v rulesetu check_*.

Pravidla a rulesety jsou velice pěkně popsány například v knize Linux – dokumentační projekt.

Řešení

Vlastní úprava spočívá v doplnění souboru .mc. Na jeho konec přidáme:

LOCAL_CONFIG
# omezeni odesilani mailu mimo firmu pro nektere adresy
# jejich seznam je v souboru sendmail.intra, je to seznam username bez domeny:

# definice tri makra z externiho souboru
F{Intra} -o /etc/mail/sendmail.intra

LOCAL_RULESETS
SLocal_check_rcpt

# stavy:
# oo    osetruji adresy, muzu dostavat napr: novak@domena.cz, "jan novak <novak@domena.cz>,  <novak@domena.cz> "jan novak", ...
# ?     je lokalni odesilatel?
# I     odesilatel je lokalni, je ze skupiny Intra?
# II    odes. je lokalni a z intra, prijemce je lokalni?
# OK    prijemce je lokalni
# DOVN  odesilatel je zvenku a snazi se mi psat nekomu z intra - zakazano

# osetrujeme adresu prijemce, je mnoho kombinaci,
# pro nas je dulezita cast v hranatych zavorkach, zbytek zahazujeme
#pridavame si <oo> na zacatek
R$*                     $: <oo> < $1 >
#odrezavame zbytecnosti z leve strany
R<oo>$+ < $* >          $: <oo> < $2 >
#tez pravou odrezavame
R<oo>< $* > $+          $: <oo> < $1 >
#odstranujeme nasi znacku <oo>
R<oo>< $+ >             $: $1


# vlastni testovani:
# pridame si hosta odesilatele, ktereho zkoumame jako prvniho:
R$*                             $: <?> < $1 > < $&{mail_host} >
# test, zda je odesilatel lokalni. kdyz je posledni cast <>, je odesilatel lokalni
# (pokud je lokalni odesilatel nebo prijemce, tak "promenny" mail_addr, mail_host, rcpt_host jsou prazdne
# takze testuju na prazdnost):
# Kdyz je splnena podminka => odesilatel je lokalni
R<?> <$*> <$@>                  $: <I> < $1 > < $&{mail_addr} >
# predchozi podminka neni splnena => odes neni lokalni -> je to smer DOVNitr
R<?> <$*> <$*>                  $: <DOVN> < $1 >
# vim, ze odesilatel je lokalni, je i ze skupiny Intra?
# kdyz je podminka splnena, odesilatel je z intra a jdu zkoumat prijemce
R<I> <$*> <$={Intra}>           $: <II> <$1> < $&{rcpt_host} >
# odesilatel je lokalni a neni z Intra, takze muze mailovat kam chce -> OK
R<I> <$*> <$*>                  $: <OK> < $1 >
# odesilatel je z intra, tedy omezovany. muze mailovat jen lokalne -> OK
# kdyz je podminka splnena, posila mail lokalnimu prijemci a to muze
R<II> <$*> <$@>                 $: <OK> < $1 >
# zakazujeme mu posilat ven
R<II> <$*> <$*>                 $#error $@ 5.7.1 $: "550 Ty smis mailovat jen v ramci firmy"
# zde testovat, zda se nesnazi zvenku mailovat na zakazane uzivatele
# vim, ze odesilatel je zvenku, kdo je prijemce?
R<DOVN> <$* @ $*>               $: <DOVN> < $1 >
R<DOVN> <$={Intra}>             $#error $@ 5.7.1 $: "550 Tomuto uzivateli nelze poslat mail, piste na info@firma.cz"
R<DOVN> <$*>                    $: <OK> < $1 >
R<OK> <$*>                      $: $1       odrezavam <OK>

Soubor s omezenými uživateli se jmenuje sendmail.intra, je uložen ve stejném adresáři jako sendmail.cf a vypadá takto:

novak
pm
petr.macek

Už zbývá jen z .mc vygenerovat sendmail.cf a ten použít. Používám FreeBSD a tam to jde velmi jednoduše (v linuxech asi trochu jinak):

cd /etc/mail
make
make install

make stop
make start

Jednoduché testování (posílám z internetu na omezeného uživatele):

pm@minastirith pm $ telnet mail.firma.cz 25
Trying ip.ad.re.sa...
Connected to mail.firma.cz.
Escape character is '^]'.
220 mail.firma.cz ESMTP Sendmail 8.13.1/8.13.1; Sun, 3 Dec 2006 20:49:04 +0100 (CET)
ehlo mail.jinde.cz
250-mail.firma.cz Hello [ip.ad.re.sa], pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE
250-DSN
250-ETRN
250-AUTH LOGIN PLAIN
250-DELIVERBY
250 HELP
mail from: pm@jinde.cz
250 2.1.0 pm@jinde.cz... Sender ok
rcpt to: pm@firma.cz
550 5.7.1 pm@firma.cz... Tomuto uzivateli nelze poslat mail, piste na info@firma.cz

Pokud narazíte na problémy nebo výjimky (sendmail může být nakonfigurován mnoha způsoby), je možné debugovat. Pusťte si sendmail -bt -C /etc/mail/sen­dmail.cf a můžete začít ladit. Jako vstup zadáváte ruleset (nebo více a pak adresu, kterou chcete zpracovát). Takto například zjistíte, jak sendmail v sadě 3 upravuje adresu pro použití v dalších rulesetech:

bash-2.05b# sendmail -bt
ADDRESS TEST MODE (ruleset 3 NOT automatically invoked)
Enter <ruleset> <address>
> 3 novak@firma.cz
canonify           input: novak @ firma . cz
Canonify2          input: novak < @ firma . cz >
Canonify2        returns: novak < @ firma . cz . >
canonify         returns: novak < @ firma . cz . >

V tomto případě je zpracován i ruleset 0 a na konci určeno, kam pošta půjde:

> 3,0 novak@firma.cz
canonify           input: novak @ firma . cz
Canonify2          input: novak < @ firma . cz >
Canonify2        returns: novak < @ firma . cz . >
canonify         returns: novak < @ firma . cz . >
parse              input: novak < @ firma . cz . >
Parse0             input: novak < @ firma . cz . >
Parse0           returns: novak < @ firma . cz . >
ParseLocal         input: novak < @ firma . cz . >
ParseLocal       returns: novak < @ firma . cz . >
Parse1             input: novak < @ firma . cz . >
Mailertable        input: < firma . cz > novak < @ firma . cz . >
Mailertable        input: firma . < cz > novak < @ firma . cz . >
Mailertable      returns: novak < @ firma . cz . >
Mailertable      returns: novak < @ firma . cz . >
MailerToTriple     input: < > novak < @ firma . cz . >
MailerToTriple   returns: novak < @ firma . cz . >
Parse1           returns: $# esmtp $@ firma . cz . $: novak < @ firma . cz . >
parse            returns: $# esmtp $@ firma . cz . $: novak < @ firma . cz . >

Pokud omezovaný uživatel bude posílat poštu mimo svou doménu, sendmail ji odmítne s hláškou 550 Ty smis mailovat jen v ramci firmy. Pokud naopak na omezenou adresu bude psát někdo z internetu, vrátí se mu mail s hláškou 550 Tomuto uzivateli nelze poslat mail, piste na info@firma.cz.

Se sendmailem se dá dělat spousta zajímavých věcí. Uvedu pár důležitých odkazů:

sendmail.org
dokumentace k .cf

root_podpora

Linux – dokumentační projekt

Velký dík patří Danovi Lukešovi za rady a připomínky.

Používáte Sendmail?

Byl pro vás článek přínosný?

Autor článku

Petr Macek studoval aplikovanou informatiku na Jihočeské univerzitě, pracuje jako síťový specialista ve firmě Kostax, s. r. o. Baví ho především FreeBSD, sítě a monitoring Cacti.