Ovaj vodič pokazuje kako razmišljati o sigurnoj PHP kontakt formi koja šalje poruke preko
SMTP-a, umjesto oslanjanja na PHP funkciju mail(). Primjeri su namjerno
praktični: cilj nije napraviti framework, nego stabilan obrazac koji se može prilagoditi
custom poslovnim web stranicama.
mail() često ovisi o konfiguraciji hostinga, zna biti ugašena zbog
zloupotrebe i ne daje dovoljnu kontrolu nad autentifikacijom. SMTP s provjerenim računom,
TLS-om i jasnim logiranjem je predvidljiviji, sigurniji i lakši za dijagnostiku.
1. Što kontakt forma mora riješiti
Kontakt forma nije samo HTML forma i slanje emaila. U produkciji treba riješiti barem pet problema: korisnik mora moći poslati poruku, botovi ne smiju zatrpati inbox, podaci moraju biti validirani, mail mora proći kroz pouzdan kanal, a greške moraju biti vidljive developeru bez otkrivanja osjetljivih informacija korisniku.
Najčešća greška je tretirati kontakt formu kao “mali feature” koji se samo zalijepi na kraj. Ako forma ne radi, poslovna web stranica gubi upite. Ako forma radi nesigurno, može postati spam relay, izvor log smeća ili sigurnosni rizik.
Minimalni produkcijski standard
- Server-side validacija svih polja, ne samo HTML5 validacija u browseru.
- CSRF token za zaštitu od neželjenih cross-site submitova.
- Honeypot polje ili drugi tihi anti-spam signal.
- SMTP slanje preko autentificiranog računa i TLS/SSL zaštite.
- Konfiguracija izvan javnog koda, bez hardcodanih lozinki u templateu.
- Logiranje tehničkih grešaka bez prikaza SMTP detalja korisniku.
2. Predložena struktura datoteka
Kod manjih custom webova struktura može ostati jednostavna. Bitno je da se javni handler, konfiguracija i pomoćne klase ne miješaju u jednu nečitljivu datoteku. Ne treba uvoditi pretežak framework samo za kontakt formu, ali treba jasno razdvojiti odgovornosti.
project/
├── config.php
├── lib/
│ ├── mailer.php
│ └── validation.php
├── pages/
│ └── kontakt.php
├── public/
│ └── form-handler.php
└── vendor/
└── phpmailer/
U stvarnom projektu putanje mogu biti drugačije, ali princip je isti: konfiguracija je centralizirana, PHPMailer setup je izdvojen, validacija je izdvojena, a handler samo orkestrira proces.
3. Konfiguracija: tajne ne smiju živjeti u HTML-u
SMTP lozinka, host, port i korisničko ime ne bi trebali biti razbacani po templateima. Idealno je koristiti environment varijable, ali na manjim hosting paketima često je realističnije imati produkcijski config izvan repozitorija ili barem config koji se ne commita s pravim tajnama.
<?php
return [
'smtp' => [
'host' => 'mail.example.com',
'port' => 587,
'secure' => 'tls',
'username' => 'website@example.com',
'password' => 'use-a-real-secret-outside-git',
'from_email' => 'website@example.com',
'from_name' => 'Website kontakt forma',
'to_email' => 'info@example.com',
],
];
config.php poseban za server, kod deploya ga ne treba
prepisivati cijelog. Sigurnije je mijenjati samo ono što je stvarno potrebno, primjerice
cache-busting assembly vrijednost, a SMTP tajne ostaviti netaknute.
4. HTML forma: jednostavna, ali s CSRF i honeypotom
Forma treba biti razumljiva korisniku, ali i dovoljno zaštićena od osnovnog bot prometa. Honeypot polje je skriveno polje koje normalan korisnik ne ispunjava, ali ga bot često popuni jer automatski šalje sva polja. CSRF token veže submit uz korisničku sesiju.
<?php
session_start();
$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));
?>
<form method="post" action="/form-handler.php" novalidate>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES) ?>">
<label>
Ime
<input type="text" name="name" autocomplete="name" required>
</label>
<label>
Email
<input type="email" name="email" autocomplete="email" required>
</label>
<label>
Poruka
<textarea name="message" rows="6" required></textarea>
</label>
<input type="text" name="website" tabindex="-1" autocomplete="off" class="visually-hidden">
<button type="submit">Pošalji</button>
</form>
Client-side validacija je korisna za UX, ali nije sigurnosna mjera. Sve što dođe s forme mora se provjeriti na serveru jer bot ili napadač ne mora koristiti vaš HTML.
5. Server-side validacija
Validacija treba biti stroga, ali ne smije biti iritantna. Ime ne treba prihvatiti ako je prazno ili predugo. Email mora biti stvarna email adresa. Poruka treba imati minimalnu i maksimalnu duljinu. Honeypot mora biti prazan. CSRF token mora odgovarati tokenu u sesiji.
<?php
function clean_string(?string $value): string
{
return trim((string) $value);
}
function validate_contact_input(array $post, array $session): array
{
$errors = [];
$name = clean_string($post['name'] ?? '');
$email = clean_string($post['email'] ?? '');
$message = clean_string($post['message'] ?? '');
$csrf = clean_string($post['csrf_token'] ?? '');
$honeypot = clean_string($post['website'] ?? '');
if ($honeypot !== '') {
$errors[] = 'Spam signal detected.';
}
if ($csrf === '' || !hash_equals($session['csrf_token'] ?? '', $csrf)) {
$errors[] = 'Invalid form token.';
}
if ($name === '' || mb_strlen($name) > 120) {
$errors[] = 'Name is required and must be shorter than 120 characters.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'A valid email address is required.';
}
if (mb_strlen($message) < 10 || mb_strlen($message) > 5000) {
$errors[] = 'Message must be between 10 and 5000 characters.';
}
return [$errors, compact('name', 'email', 'message')];
}
6. PHPMailer SMTP setup
PHPMailer je praktičan jer rješava MIME poruke, header encoding, SMTP autentifikaciju, TLS/SSL i dio edge caseova koje ne želite ručno pisati. Najvažnije je ne koristiti korisnikov email kao stvarni sender. Korisnikov email stavite kao Reply-To, a From neka bude domena ili mailbox koji SMTP server smije slati.
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
function send_contact_email(array $config, array $data): void
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = $config['host'];
$mail->Port = (int) $config['port'];
$mail->SMTPAuth = true;
$mail->Username = $config['username'];
$mail->Password = $config['password'];
$mail->SMTPSecure = $config['secure'] === 'ssl'
? PHPMailer::ENCRYPTION_SMTPS
: PHPMailer::ENCRYPTION_STARTTLS;
$mail->CharSet = 'UTF-8';
$mail->setFrom($config['from_email'], $config['from_name']);
$mail->addAddress($config['to_email']);
$mail->addReplyTo($data['email'], $data['name']);
$mail->Subject = 'Nova poruka s kontakt forme';
$mail->Body = sprintf(
"Ime: %s\nEmail: %s\n\nPoruka:\n%s",
$data['name'],
$data['email'],
$data['message']
);
$mail->send();
}
7. Handler koji spaja validaciju i slanje
Handler treba biti kratak i jasan. Ne smije prikazati SMTP lozinku, stack trace ili detalje servera korisniku. Korisnik treba dobiti jednostavnu poruku, a developer treba dobiti log koji pomaže riješiti problem.
<?php
session_start();
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../lib/validation.php';
require __DIR__ . '/../lib/mailer.php';
$config = require __DIR__ . '/../config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method not allowed');
}
[$errors, $data] = validate_contact_input($_POST, $_SESSION);
if ($errors !== []) {
$_SESSION['form_error'] = 'Provjerite unesene podatke i pokušajte ponovno.';
header('Location: /kontakt');
exit;
}
try {
send_contact_email($config['smtp'], $data);
unset($_SESSION['csrf_token']);
$_SESSION['form_success'] = 'Poruka je uspješno poslana.';
} catch (Throwable $e) {
error_log('Contact form SMTP error: ' . $e->getMessage());
$_SESSION['form_error'] = 'Poruku trenutno nije moguće poslati. Pokušajte ponovno kasnije.';
}
header('Location: /kontakt');
exit;
8. Rate limit bez velikog sustava
Ako ne koristite Cloudflare, reCAPTCHA ili vanjski anti-spam servis, možete dodati vrlo jednostavan session-based rate limit. To nije neprobojna zaštita, ali smanjuje slučajno floodanje i dio jednostavnih bot pokušaja.
<?php
function can_submit_contact_form(array &$session, int $cooldownSeconds = 45): bool
{
$now = time();
$lastSubmit = (int) ($session['last_contact_submit'] ?? 0);
if ($lastSubmit > 0 && ($now - $lastSubmit) < $cooldownSeconds) {
return false;
}
$session['last_contact_submit'] = $now;
return true;
}
Za ozbiljniji traffic bolji je IP-based rate limit, firewall pravila, Cloudflare Turnstile ili drugi anti-abuse sloj. Ali i ovakav mali limit je bolji od potpuno otvorenog submitanja.
9. Što logirati, a što ne
Logovi trebaju pomoći developeru, ali ne smiju postati sigurnosni problem. U logovima ne trebaju završiti SMTP lozinke, cijeli sadržaj poruke, osobni podaci bez potrebe ili detalji koji bi napadaču pomogli. Dovoljno je logirati tip greške, vrijeme, eventualno IP hash i osnovni kontekst.
<?php
function log_contact_error(Throwable $e): void
{
error_log(sprintf(
'Contact form failed at %s: %s',
date('c'),
$e->getMessage()
));
}
10. Testiranje prije produkcije
Kontakt formu treba testirati kao mali backend feature. Provjerite uspješan submit, krivi email, praznu poruku, predugu poruku, popunjen honeypot, neispravan CSRF token, ugašen SMTP račun i krivu lozinku. Ako sve to radi predvidljivo, forma je puno bliže produkcijskoj stabilnosti.
Test checklist
- Poruka stiže u inbox i Reply-To vodi na korisnikovu email adresu.
- Forma ne šalje email kada je CSRF token pogrešan.
- Honeypot submit se odbija bez slanja maila.
- Korisniku se ne prikazuje SMTP greška ili lozinka.
- Log sadrži dovoljno informacija za debug.
- Forma i dalje radi ako hosting ugasi PHP
mail().
Zaključak: kontakt forma je mali feature s velikom odgovornošću
Sigurna PHP kontakt forma ne mora biti komplicirana, ali mora biti disciplinirana. SMTP, PHPMailer, validacija, CSRF, honeypot i kontrolirano logiranje čine veliku razliku između forme koja “radi na testu” i forme koja stabilno radi u produkciji.
Ako radite custom web, ovakav pristup se dobro uklapa u širu tehničku arhitekturu: čisti backend, server-side rendering, jasna konfiguracija i minimalan broj nepotrebnih ovisnosti. Za širi kontekst pročitajte i PHP vs .NET za poslovne web stranice te razvoj web stranica po mjeri.