Skip to content
Plugin PantryPlugins

Plugins / Pantry Contact

Plugin Pantry

Pantry Contact

v1.0.0

Contents: One contact form. Entries saved. Spam trapped.

31 KB, no external requests

A single contact form that works out of the box. Tick the fields you want, drop in the shortcode or block, and every message is emailed to you and kept in the dashboard. Spam is caught with a honeypot and a timing check, not a CAPTCHA.

Compared with the popular plugins

The Pantry Contact settings screen in the WordPress admin, at Contact (top level, owns the entries post type).

What it does

  • One form with a fixed field set: name, email, phone, subject, message, consent checkbox; each can be enabled, required and relabelled
  • Shortcode and a block that renders on the server with no build step
  • Honeypot field, minimum-time check, nonce and a per-address rate limit
  • Entries saved as a private post type with a list table showing sender, subject and date, and a CSV export
  • Notification email to one or more addresses with reply-to set to the sender
  • Works without JavaScript: the form posts and redirects with a success message; a small script submits inline when available
  • About thirty lines of CSS that inherit the theme, loaded only on pages that show the form

What it does not do

  • No second form
  • No custom fields or field builder
  • No file uploads
  • No third-party integrations
  • No CAPTCHA

Measured footprint

Zip size
31 KB
Lines of PHP
2,256
Options
pantry_contact_settings, pantry_contact_version
Post meta
_pantry_contact_name, _pantry_contact_email, _pantry_contact_phone, _pantry_contact_consent, _pantry_contact_source, _pantry_contact_ip
Custom tables
none
Post types
pantry_entry
Cron events
none
Transients
pantry_contact_rl_<hashed address>, one hour
Admin assets
includes/core/assets/pantry-admin.css on the settings screen only, includes/core/assets/pantry-admin.js on the settings screen only, src/block/editor.js in the block editor only, assets/form.css in the block editor only, so the block preview matches the front end
Front-end assets
assets/form.css on pages that render the form, when the stylesheet setting is on, assets/form.js on pages that render the form, when inline submit is on
External requests
none
Measured on
2026-09-12

Compatibility

WordPress
6.4 or newer, tested to 7.1
PHP
8.1 or newer
Where it lives
Contact (top level, owns the entries post type)
Licence
GPLv2 or later

Documentation

One contact form. Entries saved. Spam trapped.

A single contact form with a fixed field set. Tick the fields you want, put the shortcode or the block on a page, and every message is emailed to you and kept in the dashboard. Spam is caught with a honeypot, a timing check and a per address rate limit. There is no CAPTCHA and no second form.

Using it

  1. Activate the plugin. A Contact menu appears with Entries and Settings.
  2. Open Contact > Settings and choose the fields, the wording and where the notification goes.
  3. Put the form on a page, either way round:
    • the shortcode [pantry_contact_form]
    • the block Contact form (under Widgets in the inserter)

The email address field is always shown and always required: it is the Reply-To address on the notification, so you can answer the message straight from your inbox.

Without JavaScript

The form is a plain POST to the page it sits on. The handler checks it, stores it, sends the email and redirects back to the same page with ?pantry-contact=sent, where the success message is shown in place of the form. Nothing about this needs JavaScript.

When JavaScript is available and Inline submit is on, a small script sends the same request to admin-ajax.php and swaps the form for the message without reloading. If the request fails, the browser falls back to the plain post.

Spam handling

Four checks run in this order, before anything is stored:

  1. Nonce. A WordPress nonce tied to the form.
  2. Honeypot. A hidden pantry_contact_website field that must stay empty.
  3. Timing. A signed timestamp is put in the form when it is rendered. The signature is a wp_hash() of the timestamp, so it cannot be forged, and the message is rejected if it comes back faster than the minimum time or more than a day later.
  4. Rate limit. A transient counts submissions per address per hour. The address is hashed with a salt before it is used as a key.

The visitor's address is never stored in full. The last block is dropped (203.0.113.45 becomes 203.0.113.0) before it is saved with the entry or put in the notification email.

Settings

All on Contact > Settings, stored in one option, pantry_contact_settings.

Form fields
Setting Default What it does
Fields shown Name, Subject, Message Which of Name, Phone, Subject, Message and Consent appear. Email is always shown.
Fields required Name, Message Which of those must be filled in. A field that is required but not shown is ignored.
Name label Name Label for the name field.
Email label Email Label for the email field.
Phone label Phone Label for the phone field.
Subject label Subject Label for the subject field. Also becomes the entry title.
Message label Message Label for the message field.
Consent wording I agree to this site storing my message so that it can reply. The text next to the consent checkbox.
Form behaviour
Setting Default What it does
Button label Send message Text on the submit button.
Success message Thank you. Your message has been sent. Shown after a message goes through.
Form stylesheet On Loads assets/form.css on pages that show the form. Turn it off to style the form entirely in your theme. The block editor loads it either way so the preview matches.
Inline submit On Loads assets/form.js so the form submits without a page reload.
Notification email
Setting Default What it does
Send to empty One address per line. Empty means the site admin address. Anything that is not a valid address is dropped on save.
Email subject New enquiry from {site} Placeholders {site}, {name} and {subject} are replaced.
Spam and limits
Setting Default What it does
Minimum time to fill in 3 Seconds. A form sent faster than this is rejected. 0 switches the check off.
Messages per hour 5 How many messages one address may send in an hour.
Keep entries On Save messages in the dashboard. Off means the notification email only.

Entries

Every accepted message becomes one pantry_entry post: the subject is the title, the message is the content, and the rest is post meta. The list table shows the subject, the sender and the first few words of the message. Only users with manage_options can see or edit them, and nothing can create one from the admin screens: entries only come from the form.

Export CSV sits above the list and on the settings screen. It goes through admin-post.php with a nonce and a capability check, and streams every entry as CSV with the date, subject, name, email, phone, consent, message, page and masked address.

Filters and actions

Everything is prefixed pantry_contact_.

pantry_contact_email_to filters the notification recipients.

add_filter( 'pantry_contact_email_to', function ( array $to ): array {
	$to[] = 'sales@example.com';
	return $to;
} );

pantry_contact_email_subject filters the subject after the placeholders are replaced. Second argument is the sanitised field values.

add_filter( 'pantry_contact_email_subject', function ( string $subject, array $data ): string {
	return '[Website] ' . $subject;
}, 10, 2 );

pantry_contact_email_body filters the plain text body, and pantry_contact_email_headers filters the headers array. Both take the field values as a second argument.

add_filter( 'pantry_contact_email_headers', function ( array $headers ): array {
	$headers[] = 'Bcc: archive@example.com';
	return $headers;
}, 10, 1 );

pantry_contact_errors filters the validation errors, an array of field key to error code, before the message is accepted. Add to it to reject a message.

add_filter( 'pantry_contact_errors', function ( array $errors, array $data ): array {
	if ( false !== strpos( $data['message'] ?? '', 'http://' ) ) {
		$errors['message'] = 'required';
	}
	return $errors;
}, 10, 2 );

pantry_contact_success_message filters the wording shown after a send.

pantry_contact_submitted fires after a message is stored and emailed, with the field values and the entry ID (0 when entries are not kept).

add_action( 'pantry_contact_submitted', function ( array $data, int $entry ): void {
	error_log( 'New enquiry stored as ' . $entry );
}, 10, 2 );

pantry_contact_entry_created fires with the entry ID and the field values just after the entry is stored.

pantry_contact_rejected fires with the reason a submission was turned away: nonce, spam, too_fast, expired, rate, required or email.

add_action( 'pantry_contact_rejected', function ( string $code ): void {
	if ( 'rate' === $code ) {
		error_log( 'Contact form rate limit hit' );
	}
} );

What it stores

  • Options: pantry_contact_settings, pantry_contact_version
  • Post type: pantry_entry
  • Post meta: _pantry_contact_name, _pantry_contact_email, _pantry_contact_phone, _pantry_contact_consent, _pantry_contact_source, _pantry_contact_ip
  • Transients: pantry_contact_rl_<hash>, one hour each
  • No custom tables, no cron events, no external requests

Uninstalling removes all of it, entries included.

Why this one is over 2,000 lines of PHP

The Pantry Standard aims for under 1,500 lines of PHP per plugin including the vendored core, and asks for a written reason above 2,000. This plugin measures 2,249 lines: 573 of those are the vendored includes/core settings screen, and 1,676 are the plugin itself.

The reason is that a contact form is four surfaces, not one: the settings screen (400 lines, most of it the field declarations the standard asks to be written as data), the form renderer (210), the submission handler with its four spam checks, validation and notification email (444), and the entries post type with its list table and CSV export (389). Cutting any of them would cut something in the does list. The alternative, splitting entries into a second plugin, would make a contact form that cannot show you your messages.

What it does not do

No second form, no field builder, no file uploads, no third-party integrations, no CAPTCHA. Those are out of scope by design; see the Pantry Standard.

Pro add-on (planned)

See Pantry Pro pricing
  • Multiple forms and custom fields
  • File uploads
  • Cloudflare Turnstile
  • Webhooks and mailing list integrations

None of this is in the free plugin, and none of it is switched off inside it. Pro would be a separate plugin.

Changelog

All notable changes to Pantry Contact are recorded here. The format follows Keep a Changelog, and the project uses Semantic Versioning.

1.0.0 - 2026-09-12

Added
  • One contact form with a fixed field set: name, email, phone, subject, message and a consent checkbox, each shown, required and relabelled from one screen.
  • Shortcode [pantry_contact_form] and a server-rendered Contact form block with no build step.
  • Honeypot field, signed minimum-time check, nonce and a per address rate limit.
  • Entries stored as a private pantry_entry post type with a list table showing sender, subject and date, plus a nonced CSV export.
  • Notification email to one or more addresses with Reply-To set to the sender.
  • Works without JavaScript: the form posts and redirects with a success message. A small script submits inline when JavaScript is available.
  • About thirty lines of CSS that inherit the theme, loaded only on pages that show the form.