Index: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/bootstrap.php
===================================================================
--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/bootstrap.php	(revision 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/bootstrap.php	(working copy)
@@ -0,0 +1,16 @@
+<?php
+
+/*
+ * Plugin Name: WordCamp Organizer Reminders
+ * Description: Automatically e-mail WordCamp organizers with various reminders at specified intervals.
+ * Version:     0.1
+ * Author:      Ian Dunn 
+ */
+
+require_once( __DIR__ . '/wcor-mailer.php' );
+$GLOBALS['WCOR_Mailer'] = new WCOR_Mailer();
+register_activation_hook(   __FILE__, array( $GLOBALS['WCOR_Mailer'], 'activate' ) );
+register_deactivation_hook( __FILE__, array( $GLOBALS['WCOR_Mailer'], 'deactivate' ) );
+
+require_once( __DIR__ . '/wcor-reminder.php' );
+$GLOBALS['WCOR_Reminder'] = new WCOR_Reminder();
\ No newline at end of file
Index: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-mailer.php
===================================================================
--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-mailer.php	(revision 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-mailer.php	(working copy)
@@ -0,0 +1,230 @@
+<?php
+
+/**
+ * Sends e-mails at time-based intervals and triggers
+ * @package WordCampOrganizerReminders
+ */
+class WCOR_Mailer {
+	
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		add_action( 'wcor_send_timed_emails', array( $this, 'send_timed_emails' ) );
+		add_action( 'post_updated',           array( $this, 'send_trigger_added_to_schedule' ), 10, 2 );
+	}
+
+	/**
+	 * Schedule cron job when plugin is activated  
+	 */
+	public function activate() {
+		if ( wp_next_scheduled( 'wcor_send_timed_emails' ) === false ) {
+			wp_schedule_event(
+				current_time( 'timestamp' ),
+				'daily',
+				'wcor_send_timed_emails'
+			);
+		}
+	}
+	
+	/**
+	 * Clear cron job when plugin is deactivated
+	 */
+	public function deactivate() {
+		wp_clear_scheduled_hook( 'wcor_send_timed_emails' );
+	}
+
+	/**
+	 * Wrapper for wp_mail() that adds our headers
+	 *
+	 * We want to make sure that replies go to support@wordcamp.org, rather than the fake address that WordPress sends from, but 
+	 * we don't want to be flagged as spam for forging the From header, so we set the Sender header.
+	 * @see http://stackoverflow.com/q/4728393/450127
+	 * 
+	 * @param string $to
+	 * @param string $subject
+	 * @param string $body
+	 * @return bool
+	 */
+	protected function mail( $to, $subject, $body ) {
+		$headers = array(
+			'From: WordCamp Central <support@wordcamp.org>',
+			'Sender: wordpress@' . strtolower( $_SERVER['SERVER_NAME'] )
+		);
+
+		// todo replace %%organizer_name%% with their name, etc?
+			// maybe use shortcodes so it happens automatically?
+			// maybe move other universal stuff here, like the checks for if the email has been sent or not
+		
+		// maybe move the subject prefix here, but then we'd have to know what the camp name is. but will need that for the shortcodes above too? 
+		
+		return wp_mail( $to, $subject, $body, $headers );
+	}
+
+	/**
+	 * Send e-mails that are scheduled to go out at a specific time (e.g., 3 days before the camp)
+	 */
+	public function send_timed_emails() {
+		$recent_or_upcoming_wordcamps = get_posts( array(
+			'posts_per_page'  => -1,
+			'post_type'       => 'wordcamp',
+			'meta_query'      => array(
+				array(
+					'key'     => 'Start Date (YYYY-mm-dd)',
+					'value'   => strtotime( 'now - 3 months' ),
+					'compare' => '>=',
+				)
+			)
+		) );
+		
+		$reminder_emails = get_posts( array(
+			'posts_per_page' => -1,
+			'post_type'      => WCOR_Reminder::POST_TYPE_SLUG
+		) );
+		
+		foreach ( $recent_or_upcoming_wordcamps as $wordcamp ) {
+			$organizers_email = get_post_meta( $wordcamp->ID, 'E-mail Address', true );
+			$sent_email_ids   = (array) get_post_meta( $wordcamp->ID, 'wcor_sent_email_ids', true );
+
+			if ( ! is_email( $organizers_email ) ) {
+				continue;
+			}
+			
+			foreach ( $reminder_emails as $email ) {
+
+				echo '<br>'. $wordcamp->post_title .' - '. $email->post_title .': ';
+				
+				if ( $this->timed_email_is_ready_to_send( $wordcamp, $email, $sent_email_ids ) ) {
+					/*$this->mail(
+						$organizers_email,
+						$wordcamp->post_title .' Reminder: ' . $email->post_title,		// todo apply filters?
+						$email->post_content	// todo apply filters?
+					);
+					
+					$sent_email_ids[] = $email->ID;
+					update_post_meta( $wordcamp->ID, 'wcor_sent_email_ids', $sent_email_ids );
+					sleep( 1 ); // don't send e-mails too fast, or it might increase the risk of being flagged as spam
+					*/
+					
+					echo 'yes';
+				}
+				
+				else echo 'no';
+			}
+		}
+	}
+
+	/**
+	 * Determines if a time-based e-mail is ready to be sent to a WordCamp
+	 *
+	 * E-mails should be sent if the current date matches the date that the e-mail is scheduled to be sent (e.g., 3 days before the camp starts).
+	 *
+	 * One exception to that is if a camp is added later than expected (e.g., we start sending e-mails 4 months before the start date, but a camp
+	 * isn't scheduled until 2 months before the start). When that happens, we want to send all the e-mails that they've missed.
+	 *
+	 * An exception to that exception is that we don't want to send e-mails to camps that have already been sent those e-mails manually, before we
+	 * started sending them automatically.
+	 * @todo this exception will no longer be relevant in a few months, and can be removed at that time. Approximately January 15th, 2014.
+	 *       
+	 * @param WP_Post $wordcamp
+	 * @param WP_Post $email
+	 * @param array   $sent_email_ids The IDs of emails that have already been sent to the $wordcamp post
+	 * @return bool
+	 */
+	protected function timed_email_is_ready_to_send( $wordcamp, $email, $sent_email_ids ) {
+		$ready      = false;
+		$send_when  = get_post_meta( $email->ID, 'wcor_send_when', true );
+		$start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true );
+		$end_date   = get_post_meta( $wordcamp->ID, 'End Date (YYYY-mm-dd)', true );
+		
+		if ( ! $end_date ) {
+			$end_date = $start_date;
+		}
+		
+		if ( ! in_array( $email->ID, $sent_email_ids ) ) {
+			if ( 'wcor_send_before' == $send_when ) {
+				$days_before = absint( get_post_meta( $email->ID, 'wcor_send_days_before', true ) );
+				
+				if ( $days_before ) {
+					$send_date = $start_date - ( $days_before * DAY_IN_SECONDS );
+					
+					if ( $send_date <= current_time( 'timestamp' ) ) {
+						$ready = true;
+					}
+				}
+			} elseif ( 'wcor_send_after' == $send_when ) {
+				$days_after = absint( get_post_meta( $email->ID, 'wcor_send_days_after', true ) );
+
+				if ( $days_after ) {
+					$send_date = $end_date + ( $days_after * DAY_IN_SECONDS );
+					
+					if ( $send_date <= current_time( 'timestamp' ) ) {
+						$ready = true;
+					}
+				}
+			}
+
+			if ( $send_date <= strtotime( 'October 15th, 2013' ) ) {	// todo update to the day before you deploy the code
+				// Assume it was already sent manually before this plugin was activated
+				$ready = false;
+			}
+		}
+		
+		// todo write unit tests for this function?
+			// hard to do b/c have to mock get_post_meta and current_time()
+		
+		return $ready;
+	}
+
+	/**
+	 * Sends e-mails hooked to the wcor_added_to_schedule trigger.
+	 *
+	 * This fires when a WordCamp is added to the schedule (i.e., when they set the start date in their `wordcamp` post).
+	 * 
+	 * Since Core doesn't support revisions on post meta, we're not actually checking to see if the start date was added during
+	 * the current post update, but just that it has a start data. By itself, that would lead to the e-mail being sent every time
+	 * the post is updated, but to avoid that we're checking the `wcor_sent_email_id` post meta for the `wordcamp` post to see if
+	 * we've already sent this particular e-mail in the past.
+	 *
+	 * @param int $post_id
+	 * @param WP_Post $post
+	 */
+	public function send_trigger_added_to_schedule( $post_id, $post ) {
+
+		/* @todo Update time-based code to ignore trigger-based e-mails in multiple places.		 */
+		
+		
+		
+		if ( 'wordcamp' == $post->post_type && 'publish' == $post->post_status ) {
+			$start_date       = get_post_meta( $post_id, 'Start Date (YYYY-mm-dd)', true );
+			$organizers_email = get_post_meta( $post_id, 'E-mail Address', true );
+			$sent_email_ids   = (array) get_post_meta( $post_id, 'wcor_sent_email_ids', true );
+		
+			if ( $start_date && is_email( $organizers_email ) ) {
+				$emails = get_posts( array(
+					'posts_per_page' => -1,
+					'post_type'      => WCOR_Reminder::POST_TYPE_SLUG,
+					'meta_query'     => array(
+						array(
+							'key'    => 'wcor_which_trigger',
+							'value'  => 'wcor_added_to_schedule',
+						)
+					)
+				) );
+				
+				foreach( $emails as $email ) {
+					if ( ! in_array( $email->ID, $sent_email_ids ) ) {
+						$this->mail(
+							$organizers_email,
+							$post->post_title . ' Reminder: ' . $email->post_title,
+							$email->post_body
+						);
+	
+						$sent_email_ids[] = $email->ID;
+						// todo update_post_meta( $post_id, 'wcor_sent_email_ids', $sent_email_ids );
+					}
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
Index: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-reminder.php
===================================================================
--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-reminder.php	(revision 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-organizer-reminders/wcor-reminder.php	(working copy)
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * A Custom post type to store the body of the reminder e-mails
+ * @package WordCampOrganizerReminders
+ */
+
+class WCOR_Reminder {
+	const POST_TYPE_SLUG = 'organizer-reminder';
+
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		add_action( 'init',                              array( $this, 'register_post_type' ) );
+		add_action( 'admin_init',                        array( $this, 'add_meta_boxes' ) );
+		add_action( 'save_post_' . self::POST_TYPE_SLUG, array( $this, 'save_post' ), 10, 2 );
+	}
+
+	/**
+	 * Registers the Reminder post type
+	 */
+	public function register_post_type() {
+		$labels = array(
+			'name'               => 'Organizer Reminders',
+			'singular_name'      => 'Organizer Reminder',
+			'add_new'            => 'Add New',
+			'add_new_item'       => 'Add New Reminder',
+			'edit'               => 'Edit',
+			'edit_item'          => 'Edit Reminder',
+			'new_item'           => 'New Reminder',
+			'view'               => 'View Reminders',
+			'view_item'          => 'View Reminder',
+			'search_items'       => 'Search Reminders',
+			'not_found'          => 'No reminders',
+			'not_found_in_trash' => 'No reminders',
+			'parent'             => 'Parent Reminder',
+		);
+
+		$params = array(
+			'labels'              => $labels,
+			'singular_label'      => 'Reminder',
+			'public'              => true,
+			'exclude_from_search' => true,
+			'publicly_queryable'  => false,
+			'show_ui'             => true,
+			'show_in_nav_menus'   => false,
+			'hierarchical'        => false,
+			'capability_type'     => 'post',
+			'has_archive'         => false,
+			'rewrite'             => false,
+			'query_var'           => false,
+			'supports'            => array( 'title', 'editor', 'author', 'revisions' ),
+		);
+		
+		register_post_type( self::POST_TYPE_SLUG, $params );
+	}
+
+	/**
+	 * Adds meta boxes for the custom post type
+	 */
+	public function add_meta_boxes() {
+		add_meta_box(
+			'wcor_reminder_details',
+			'Reminder Details',
+			array( $this, 'markup_reminder_details' ),
+			self::POST_TYPE_SLUG,
+			'side',
+			'default'
+		);
+	}
+
+	/**
+	 * Builds the markup for the Reminder Details metabox
+	 *
+	 * @param object $post
+	 */
+	public static function markup_reminder_details( $post ) {
+		$send_when        = get_post_meta( $post->ID, 'wcor_send_when', true );
+		$send_days_before = get_post_meta( $post->ID, 'wcor_send_days_before', true );
+		$send_days_after  = get_post_meta( $post->ID, 'wcor_send_days_after', true );
+		$which_trigger    = get_post_meta( $post->ID, 'wcor_which_trigger', true );
+		
+		?>
+		
+		<p>When should this e-mail be sent?</p>
+
+		<table>
+			<tbody>
+				<tr>
+					<th><input id="wcor_send_before" name="wcor_send_when" type="radio" value="wcor_send_before" <?php checked( $send_when, 'wcor_send_before' ); ?>></th>
+					<td><label for="wcor_send_before">before the camp starts: </label></td>
+					<td>
+						<input id="wcor_send_days_before" name="wcor_send_days_before" type="text" class="small-text" value="<?php echo esc_attr( $send_days_before ); ?>" />
+						<label for="wcor_send_days_before">days</label>
+					</td>
+				</tr>
+
+				<tr>
+					<th><input id="wcor_send_after" name="wcor_send_when" type="radio" value="wcor_send_after" <?php checked( $send_when, 'wcor_send_after' ); ?>></th>
+					<td><label for="wcor_send_after">after the camp ends: </label></td>
+					<td>
+						<input id="wcor_send_days_after" name="wcor_send_days_after" type="text" class="small-text" value="<?php echo esc_attr( $send_days_after ); ?>" />
+						<label for="wcor_send_days_after">days</label>
+					</td>
+				</tr>
+
+				<tr>
+					<th><input id="wcor_send_trigger" name="wcor_send_when" type="radio" value="wcor_send_trigger" <?php checked( $send_when, 'wcor_send_trigger' ); ?>></th>
+					<td><label for="wcor_send_trigger">on trigger: </label></td>
+					<td>
+						<select name="wcor_which_trigger">
+							<option value="null" <?php selected( $which_trigger, false ); ?>></option>
+							<option value="wcor_added_to_schedule" <?php selected( $which_trigger, 'wcor_added_to_schedule' ); ?>>Added to schedule</option>
+								
+								<!-- todo maybe make this auto build from array in WCOR_MAILER -->
+						</select>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+
+		<?php
+	}
+
+	/**
+	 * Checks to make sure the conditions for saving post meta are met
+	 * 
+	 * @param int $post_id
+	 * @param object $post
+	 */
+	public function save_post( $post_id, $post ) {
+		$ignored_actions = array( 'trash', 'untrash', 'restore' );
+
+		if ( isset( $_GET['action'] ) && in_array( $_GET['action'], $ignored_actions ) ) {
+			return;
+		}
+
+		if ( ! current_user_can( 'edit_posts', $post_id ) ) {
+			return;
+		}
+
+		if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || $post->post_status == 'auto-draft' ) {
+			return;
+		}
+		
+		$this->save_post_meta( $post, $_POST );
+	}
+
+	/**
+	 * Saves the meta data for the reminder post
+	 * 
+	 * @param WP_Post $post
+	 * @param array $new_meta
+	 */
+	protected function save_post_meta( $post, $new_meta ) {
+		if ( isset( $new_meta['wcor_send_when'] ) ) {
+			if ( in_array( $new_meta['wcor_send_when'], array( 'wcor_send_before', 'wcor_send_after', 'wcor_send_trigger' ) ) ) {
+				update_post_meta( $post->ID, 'wcor_send_when', $new_meta['wcor_send_when'] );
+			}
+		}
+
+		if ( isset( $new_meta['wcor_send_days_before'] ) ) {
+			update_post_meta( $post->ID, 'wcor_send_days_before', absint( $new_meta['wcor_send_days_before'] ) );
+		}
+
+		if ( isset( $new_meta['wcor_send_days_after'] ) ) {
+			update_post_meta( $post->ID, 'wcor_send_days_after', absint( $new_meta['wcor_send_days_after'] ) );
+		}
+
+		if ( isset( $new_meta['wcor_which_trigger'] ) ) {
+			if ( in_array( $new_meta['wcor_which_trigger'], array( 'null', 'wcor_added_to_schedule' ) ) ) {	// todo maybe build array dynamically from wcor_mailer
+				update_post_meta( $post->ID, 'wcor_which_trigger', $new_meta['wcor_which_trigger'] );
+			}
+		}
+	}
+}
\ No newline at end of file
