diff --git a/cms/pages/modules/cms_blogs.php b/cms/pages/modules/cms_blogs.php
index dccd7d8..3d7bad1 100644
--- a/cms/pages/modules/cms_blogs.php
+++ b/cms/pages/modules/cms_blogs.php
@@ -397,7 +397,7 @@ class Module_cms_blogs extends standard_aed_module
 			$start_hour=post_param_integer('schedule_hour');
 			$start_minute=post_param_integer('schedule_minute');
 			require_code('calendar2');
-			add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('PUBLISH_NEWS',$title),$schedule_code,3,0,$start_year,$start_month,$start_day,$start_hour,$start_minute);
+			add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('PUBLISH_NEWS',$title),$schedule_code,3,0,$start_year,$start_month,$start_day,'day_of_month',$start_hour,$start_minute);
 		}
 
 		return strval($id);
@@ -468,7 +468,7 @@ class Module_cms_blogs extends standard_aed_module
 				$start_day=post_param_integer('schedule_day');
 				$start_hour=post_param_integer('schedule_hour');
 				$start_minute=post_param_integer('schedule_minute');
-				add_calendar_event(db_get_first_id(),'none',NULL,0,do_lang('PUBLISH_NEWS',0,post_param('title')),$schedule_code,3,0,$start_year,$start_month,$start_day,$start_hour,$start_minute);
+				add_calendar_event(db_get_first_id(),'none',NULL,0,do_lang('PUBLISH_NEWS',0,post_param('title')),$schedule_code,3,0,$start_year,$start_month,$start_day,'day_of_month',$start_hour,$start_minute);
 			}
 		}
 
diff --git a/cms/pages/modules/cms_calendar.php b/cms/pages/modules/cms_calendar.php
index 0f64e75..b19d1a3 100644
--- a/cms/pages/modules/cms_calendar.php
+++ b/cms/pages/modules/cms_calendar.php
@@ -80,15 +80,39 @@ class Module_cms_calendar extends standard_aed_module
 
 		$this->javascript="
 			var form=document.getElementById('recurrence_pattern').form;
-			var crf=function() {
+
+			var start_day=document.getElementById('start_day');
+			var start_month=document.getElementById('start_month');
+			var start_year=document.getElementById('start_year');
+
+			var crf=function(event) {
 				var s=(form.elements['recurrence'][0].checked);
 				if (form.elements['recurrence_pattern']) form.elements['recurrence_pattern'].disabled=s;
 				if (form.elements['recurrences']) form.elements['recurrences'].disabled=s;
 				if (form.elements['seg_recurrences']) form.elements['seg_recurrences'].disabled=s;
+
+				if ((typeof event!='undefined') && (start_day.selectedIndex!=0) && (start_month.selectedIndex!=0) && (start_year.selectedIndex!=0)) // Something changed
+				{
+					var new_data=load_snippet('calendar_recurrence_suggest&monthly_spec_type='+window.encodeURIComponent(radioValue(form.elements['monthly_spec_type']))+'&day='+window.encodeURIComponent(start_day.options[start_day.selectedIndex].value)+'&month='+window.encodeURIComponent(start_month.options[start_month.selectedIndex].value)+'&year='+window.encodeURIComponent(start_year.options[start_year.selectedIndex].value));
+					var tr=form.elements['monthly_spec_type'][0];
+					while (tr.nodeName.toLowerCase()!='tr')
+					{
+						tr=tr.parentNode;
+					}
+					setInnerHTML(tr,new_data.replace(/<tr [^>]*>/,'').replace(/<\/tr>/,''));
+				}
+				var monthly_recurrence=form.elements['recurrence'][3].checked;
+				for (var i=0;i<form.elements['monthly_spec_type'].length;i++)
+				{
+					form.elements['monthly_spec_type'][i].disabled=!monthly_recurrence;
+				}
 			};
 			crf();
 			for (var i=0;i<form.elements['recurrence'].length;i++) form.elements['recurrence'][i].onclick=crf;
-			
+			start_day.onchange=crf;
+			start_month.onchange=crf;
+			start_year.onchange=crf;
+
 			var crf2=function() {
 				var s=document.getElementById('all_day_event').checked;
 				document.getElementById('start_hour').disabled=s;
@@ -223,7 +247,8 @@ class Module_cms_calendar extends standard_aed_module
 				$types[$row['e_type']]=$type;
 			}
 			
-			$time_raw=mktime($row['e_start_hour'],$row['e_start_minute'],0,$row['e_start_month'],$row['e_start_day'],$row['e_start_year']);
+			$start_day_of_month=find_concrete_day_of_month($row['e_start_year'],$row['e_start_month'],$row['e_start_day'],$row['e_start_monthly_spec_type']);
+			$time_raw=mktime($row['e_start_hour'],$row['e_start_minute'],0,$row['e_start_month'],$start_day_of_month,$row['e_start_year']);
 			$date=get_timezoned_date($time_raw,!is_null($row['e_start_hour']));
 
 			$fields->attach(results_entry(array(protect_from_escaping(hyperlink(build_url(array('page'=>'calendar','type'=>'view','id'=>$row['id']),get_module_zone('calendar')),get_translated_text($row['e_title']))),$date,$type,($row['validated']==1)?do_lang_tempcode('YES'):do_lang_tempcode('NO'),protect_from_escaping(hyperlink($edit_link,do_lang_tempcode('EDIT'),false,true,'#'.strval($row['id']))))),true);
@@ -253,6 +278,8 @@ class Module_cms_calendar extends standard_aed_module
 	 * @param  ?integer			The year the event starts at (NULL: default)
 	 * @param  ?integer			The month the event starts at (NULL: default)
 	 * @param  ?integer			The day the event starts at (NULL: default)
+	 * @param  ID_TEXT			In-month specification type for start date
+	 * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
 	 * @param  ?integer			The hour the event starts at (NULL: default)
 	 * @param  ?integer			The minute the event starts at (NULL: default)
 	 * @param  SHORT_TEXT		The title of the event
@@ -266,6 +293,8 @@ class Module_cms_calendar extends standard_aed_module
 	 * @param  ?integer			The year the event ends at (NULL: not a multi day event)
 	 * @param  ?integer			The month the event ends at (NULL: not a multi day event)
 	 * @param  ?integer			The day the event ends at (NULL: not a multi day event)
+	 * @param  ID_TEXT			In-month specification type for end date
+	 * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
 	 * @param  ?integer			The hour the event ends at (NULL: not a multi day event)
 	 * @param  ?integer			The minute the event ends at (NULL: not a multi day event)
 	 * @param  ?ID_TEXT			The timezone for the event (NULL: current user's timezone)
@@ -277,7 +306,7 @@ class Module_cms_calendar extends standard_aed_module
 	 * @param  LONG_TEXT			Notes
 	 * @return array				A tuple of: (fields, hidden-fields, delete-fields, edit-text, whether all delete fields are specified, posting form text, more fields)
 	 */
-	function get_form_fields($type=NULL,$start_year=NULL,$start_month=NULL,$start_day=NULL,$start_hour=NULL,$start_minute=NULL,$title='',$content='',$recurrence='none',$recurrences=NULL,$seg_recurrences=0,$is_public=1,$priority=3,$end_year=NULL,$end_month=NULL,$end_day=NULL,$end_hour=NULL,$end_minute=NULL,$timezone=NULL,$do_timezone_conv=0,$validated=1,$allow_rating=NULL,$allow_comments=NULL,$allow_trackbacks=NULL,$notes='')
+	function get_form_fields($type=NULL,$start_year=NULL,$start_month=NULL,$start_day=NULL,$start_monthly_spec_type='day_of_month',$start_hour=NULL,$start_minute=NULL,$title='',$content='',$recurrence='none',$recurrences=NULL,$seg_recurrences=0,$is_public=1,$priority=3,$end_year=NULL,$end_month=NULL,$end_day=NULL,$end_monthly_spec_type='day_of_month',$end_hour=NULL,$end_minute=NULL,$timezone=NULL,$do_timezone_conv=0,$validated=1,$allow_rating=NULL,$allow_comments=NULL,$allow_trackbacks=NULL,$notes='')
 	{
 		list($allow_rating,$allow_comments,$allow_trackbacks)=$this->choose_feedback_fields_statistically($allow_rating,$allow_comments,$allow_trackbacks);
 
@@ -347,9 +376,14 @@ class Module_cms_calendar extends standard_aed_module
 
 		// Dates
 		$fields->attach(form_input_tick(do_lang_tempcode('ALL_DAY_EVENT'),do_lang_tempcode('DESCRIPTION_ALL_DAY_EVENT'),'all_day_event',is_null($start_hour)));
-		$fields->attach(form_input_date(do_lang_tempcode('DATE_TIME'),'','start',false,false,true,array(is_null($start_minute)?find_timezone_start_minute_in_utc($timezone,$start_year,$start_month,$start_day):$start_minute,is_null($start_hour)?find_timezone_start_hour_in_utc($timezone,$start_year,$start_month,$start_day):$start_hour,$start_month,$start_day,$start_year),120,intval(date('Y'))-100,NULL,NULL,true,$timezone));
-		$fields->attach(form_input_date(do_lang_tempcode('END_DATE_AND_TIME'),do_lang_tempcode('DESCRIPTION_END_DATE_AND_TIME'),'end',true,is_null($end_year),true,array(is_null($end_minute)?find_timezone_end_minute_in_utc($timezone,$end_year,$end_month,$end_day):$end_minute,is_null($end_hour)?find_timezone_end_hour_in_utc($timezone,$end_year,$end_month,$end_day):$end_hour,$end_month,$end_day,$end_year),120,intval(date('Y'))-100,NULL,NULL,true,$timezone));
-
+		$start_day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+		$fields->attach(form_input_date(do_lang_tempcode('DATE_TIME'),'','start',false,false,true,array(is_null($start_minute)?find_timezone_start_minute_in_utc($timezone,$start_year,$start_month,$start_day,$start_monthly_spec_type):$start_minute,is_null($start_hour)?find_timezone_start_hour_in_utc($timezone,$start_year,$start_month,$start_day,$start_monthly_spec_type):$start_hour,$start_month,$start_day_of_month,$start_year),120,intval(date('Y'))-100,NULL,NULL,true,$timezone));
+		$end_day_of_month=find_concrete_day_of_month($end_year,$end_month,$end_day,$end_monthly_spec_type);
+		$fields->attach(form_input_date(do_lang_tempcode('END_DATE_AND_TIME'),do_lang_tempcode('DESCRIPTION_END_DATE_AND_TIME'),'end',true,is_null($end_year),true,array(is_null($end_minute)?find_timezone_end_minute_in_utc($timezone,$end_year,$end_month,$end_day,$end_monthly_spec_type):$end_minute,is_null($end_hour)?find_timezone_end_hour_in_utc($timezone,$end_year,$end_month,$end_day,$end_monthly_spec_type):$end_hour,$end_month,$end_day_of_month,$end_year),120,intval(date('Y'))-100,NULL,NULL,true,$timezone));
+		//$hidden->attach(form_input_hidden('start_monthly_spec_type',$start_monthly_spec_type));
+		//$hidden->attach(form_input_hidden('end_monthly_spec_type',$end_monthly_spec_type));
+
+		// Validation
 		if ($validated==0) $validated=get_param_integer('validated',0);
 		if (has_some_cat_specific_permission(get_member(),'bypass_validation_'.$this->permissions_require.'range_content',NULL,$this->permissions_cat_require))
 			if (addon_installed('unvalidated'))
@@ -401,6 +435,7 @@ class Module_cms_calendar extends standard_aed_module
 		$fields2->attach(form_input_line(do_lang_tempcode('RECURRENCE_PATTERN'),do_lang_tempcode('DESCRIPTION_RECURRENCE_PATTERN'),'recurrence_pattern',$recurrence_pattern,false));
 		$fields2->attach(form_input_integer(do_lang_tempcode('RECURRENCES'),do_lang_tempcode('DESCRIPTION_RECURRENCES'),'recurrences',$recurrences,false));
 		$fields2->attach(form_input_tick(do_lang_tempcode('SEG_RECURRENCES'),do_lang_tempcode('DESCRIPTION_SEG_RECURRENCES'),'seg_recurrences',$seg_recurrences==1));
+		$fields2->attach(monthly_spec_type_chooser($start_day_of_month,$start_month,$start_year,$start_monthly_spec_type));
 
 		if (($adding) && (cron_installed())) // Some more stuff only when adding
 		{
@@ -462,6 +497,7 @@ class Module_cms_calendar extends standard_aed_module
 			$start_day=INTEGER_MAGIC_NULL;
 			$start_hour=INTEGER_MAGIC_NULL;
 			$start_minute=INTEGER_MAGIC_NULL;
+			$start_monthly_spec_type=STRING_MAGIC_NULL;
 		} else
 		{
 			$start_year=intval(date('Y',$start));
@@ -476,6 +512,11 @@ class Module_cms_calendar extends standard_aed_module
 				$start_hour=intval(date('H',$start));
 				$start_minute=intval(date('i',$start));
 			}
+			$start_monthly_spec_type=post_param('start_monthly_spec_type',post_param('monthly_spec_type','day_of_month')); // We actually don't suppose separate spec-types for the ends and starts in the UI
+			if ($start_monthly_spec_type!='day_of_month')
+			{
+				$start_day=find_abstract_day($start_year,$start_month,$start_day,$start_monthly_spec_type);
+			}
 		}
 		if (fractional_edit())
 		{
@@ -484,6 +525,7 @@ class Module_cms_calendar extends standard_aed_module
 			$end_day=INTEGER_MAGIC_NULL;
 			$end_hour=INTEGER_MAGIC_NULL;
 			$end_minute=INTEGER_MAGIC_NULL;
+			$end_monthly_spec_type=STRING_MAGIC_NULL;
 		} else
 		{
 			$end=get_input_date('end');
@@ -502,8 +544,15 @@ class Module_cms_calendar extends standard_aed_module
 					$end_minute=intval(date('i',$end));
 				}
 
-				// Error if wrong way around
-				if ($start>$end) warn_exit(do_lang_tempcode('EVENT_CANNOT_AROUND'));
+				$end_monthly_spec_type=post_param('end_monthly_spec_type',$start_monthly_spec_type);
+				if ($end_monthly_spec_type!='day_of_month')
+				{
+					$end_day=find_abstract_day($end_year,$end_month,$end_day,$end_monthly_spec_type);
+				} else
+				{
+					// Error if wrong way around
+					if ($start>$end) warn_exit(do_lang_tempcode('EVENT_CANNOT_AROUND'));
+				}
 			} else
 			{
 				$end_year=NULL;
@@ -511,10 +560,17 @@ class Module_cms_calendar extends standard_aed_module
 				$end_day=NULL;
 				$end_hour=NULL;
 				$end_minute=NULL;
+				$end_monthly_spec_type='day_of_month';
 			}
 		}
 
-		return array($type,$recurrence,$recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv);
+		if ($recurrence!='monthly')
+		{
+			$start_monthly_spec_type='day_of_month';
+			$end_monthly_spec_type='day_of_month';
+		}
+
+		return array($type,$recurrence,$recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$do_timezone_conv);
 	}
 
 	/**
@@ -562,7 +618,7 @@ class Module_cms_calendar extends standard_aed_module
 
 		check_edit_permission(($myrow['e_is_public']==1)?'mid':'low',$myrow['e_submitter']);
 		$content=get_translated_text($myrow['e_content']);
-		$fields=$this->get_form_fields($myrow['e_type'],$myrow['e_start_year'],$myrow['e_start_month'],$myrow['e_start_day'],$myrow['e_start_hour'],$myrow['e_start_minute'],get_translated_text($myrow['e_title']),$content,$myrow['e_recurrence'],$myrow['e_recurrences'],$myrow['e_seg_recurrences'],$myrow['e_is_public'],$myrow['e_priority'],$myrow['e_end_year'],$myrow['e_end_month'],$myrow['e_end_day'],$myrow['e_end_hour'],$myrow['e_end_minute'],$myrow['e_timezone'],$myrow['e_do_timezone_conv'],$myrow['validated'],$myrow['allow_rating'],$myrow['allow_comments'],$myrow['allow_trackbacks'],$myrow['notes'],$myrow['validated']);
+		$fields=$this->get_form_fields($myrow['e_type'],$myrow['e_start_year'],$myrow['e_start_month'],$myrow['e_start_day'],$myrow['e_start_monthly_spec_type'],$myrow['e_start_hour'],$myrow['e_start_minute'],get_translated_text($myrow['e_title']),$content,$myrow['e_recurrence'],$myrow['e_recurrences'],$myrow['e_seg_recurrences'],$myrow['e_is_public'],$myrow['e_priority'],$myrow['e_end_year'],$myrow['e_end_month'],$myrow['e_end_day'],$myrow['e_end_monthly_spec_type'],$myrow['e_end_hour'],$myrow['e_end_minute'],$myrow['e_timezone'],$myrow['e_do_timezone_conv'],$myrow['validated'],$myrow['allow_rating'],$myrow['allow_comments'],$myrow['allow_trackbacks'],$myrow['notes'],$myrow['validated']);
 
 		if (has_delete_permission('low',get_member(),$myrow['e_submitter'],'cms_calendar'))
 		{
@@ -582,7 +638,7 @@ class Module_cms_calendar extends standard_aed_module
 	 */
 	function add_actualisation()
 	{
-		list($type,$recurrence,$recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv)=$this->get_event_parameters();
+		list($type,$recurrence,$recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$do_timezone_conv)=$this->get_event_parameters();
 
 		$allow_trackbacks=post_param_integer('allow_trackbacks',0);
 		$allow_rating=post_param_integer('allow_rating',0);
@@ -591,7 +647,7 @@ class Module_cms_calendar extends standard_aed_module
 		$validated=post_param_integer('validated',0);
 		$seg_recurrences=post_param_integer('seg_recurrences',0);
 
-		$id=add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
+		$id=add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$do_timezone_conv,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
 
 		// Reminders
 		if (function_exists('set_time_limit')) @set_time_limit(0);
@@ -675,22 +731,23 @@ class Module_cms_calendar extends standard_aed_module
 
 		regenerate_event_reminder_jobs($id);
 
-		$conflicts=detect_conflicts(get_member(),$id,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences);
+		$conflicts=detect_conflicts(get_member(),$id,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$start_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences);
 		$_description=is_null($conflicts)?paragraph(do_lang_tempcode('SUBMIT_THANKYOU')):$conflicts;
 
 		$this->donext_type=$type;
-		$this->donext_date=strval($start_year).'-'.strval($start_month).'-'.strval($start_day);
+		$start_day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+		$this->donext_date=strval($start_year).'-'.strval($start_month).'-'.strval($start_day_of_month);
 
 		if ($validated==1)
 		{
 			if ((has_actual_page_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar')) && (has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar',strval($type))))
 			{
-				$_from=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_hour,$start_minute,true);
+				$_from=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,true);
 				$from=cal_utctime_to_usertime($_from,$timezone,false);
 				$to=mixed();
 				if (!is_null($end_year) && !is_null($end_month) && !is_null($end_day))
 				{
-					$_to=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_hour,$end_minute,true);
+					$_to=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,true);
 					$to=cal_utctime_to_usertime($_to,$timezone,false);
 				}
 
@@ -720,7 +777,7 @@ class Module_cms_calendar extends standard_aed_module
 
 		$delete_status=post_param('delete','0');
 
-		list($type,$recurrence,$_recurrences,$title,$content,$priority,$is_public,$_start_year,$_start_month,$_start_day,$_start_hour,$_start_minute,$_end_year,$_end_month,$_end_day,$_end_hour,$_end_minute,$timezone,$do_timezone_conv)=$this->get_event_parameters();
+		list($type,$recurrence,$_recurrences,$title,$content,$priority,$is_public,$_start_year,$_start_month,$_start_day,$start_monthly_spec_type,$_start_hour,$_start_minute,$_end_year,$_end_month,$_end_day,$end_monthly_spec_type,$_end_hour,$_end_minute,$timezone,$do_timezone_conv)=$this->get_event_parameters();
 		if ($delete_status!='3')
 		{
 			$start_year=$_start_year;
@@ -747,7 +804,7 @@ class Module_cms_calendar extends standard_aed_module
 		if (($delete_status=='3') && (!fractional_edit()))
 		{
 			// Fix past occurences
-			$past_times=find_periods_recurrence($event['e_timezone'],1,$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_hour'],$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_hour'],$event['e_end_minute'],$event['e_recurrence'],$event['e_recurrences'],utctime_to_usertime(mktime($event['e_start_hour'],$event['e_start_minute'],0,$event['e_start_month'],$event['e_start_day'],$event['e_start_year'])),utctime_to_usertime(time()));
+			$past_times=find_periods_recurrence($event['e_timezone'],1,$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],$event['e_start_hour'],$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],$event['e_end_hour'],$event['e_end_minute'],$event['e_recurrence'],$event['e_recurrences'],utctime_to_usertime(mktime($event['e_start_hour'],$event['e_start_minute'],0,$event['e_start_month'],$event['e_start_day'],$event['e_start_year'])),utctime_to_usertime(time()));
 			foreach ($past_times as $past_time)
 			{
 				list($start_year,$start_month,$start_day,$start_hour,$start_minute)=explode('-',date('Y-m-d-h-i',usertime_to_utctime($past_time[0])));
@@ -763,7 +820,7 @@ class Module_cms_calendar extends standard_aed_module
 					$end_hour=intval($explode[3]);
 					$end_minute=intval($explode[4]);
 				}
-				add_calendar_event($event['e_type'],'none',NULL,0,get_translated_text($event['e_title']),get_translated_text($event['e_content']),$event['e_priority'],$event['e_is_public'],intval($start_year),intval($start_month),intval($start_day),intval($start_hour),intval($start_minute),$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
+				add_calendar_event($event['e_type'],'none',NULL,0,get_translated_text($event['e_title']),get_translated_text($event['e_content']),$event['e_priority'],$event['e_is_public'],intval($start_year),intval($start_month),intval($start_day),'day_of_month',intval($start_hour),intval($start_minute),$end_year,$end_month,$end_day,'day_of_month',$end_hour,$end_minute,$timezone,$do_timezone_conv,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
 			}
 			if (is_null($_recurrences))
 			{
@@ -787,7 +844,7 @@ class Module_cms_calendar extends standard_aed_module
 				$end_hour=$_end_hour;
 				$end_minute=$_end_minute;
 			}
-			$past_times=find_periods_recurrence($event['e_timezone'],1,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$event['e_recurrence'],1,time());
+			$past_times=find_periods_recurrence($event['e_timezone'],1,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$event['e_recurrence'],1,time());
 			if (array_key_exists(0,$past_times))
 			{
 				$past_time=$past_times[0];
@@ -820,12 +877,12 @@ class Module_cms_calendar extends standard_aed_module
 		{
 			if ((has_actual_page_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar')) && (has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar',strval($type))))
 			{
-				$_from=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_hour,$start_minute,true);
+				$_from=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,true);
 				$from=cal_utctime_to_usertime($_from,$timezone,false);
 				$to=mixed();
 				if (!is_null($end_year) && !is_null($end_month) && !is_null($end_day))
 				{
-					$_to=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_hour,$end_minute,true);
+					$_to=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,true);
 					$to=cal_utctime_to_usertime($_to,$timezone,false);
 				}
 
@@ -833,18 +890,19 @@ class Module_cms_calendar extends standard_aed_module
 			}
 		}
 
-		edit_calendar_event($id,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv,post_param('meta_keywords',STRING_MAGIC_NULL),post_param('meta_description',STRING_MAGIC_NULL),$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
+		edit_calendar_event($id,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$do_timezone_conv,post_param('meta_keywords',STRING_MAGIC_NULL),post_param('meta_description',STRING_MAGIC_NULL),$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
 
 		if (!fractional_edit())
 		{
-			$conflicts=detect_conflicts(get_member(),$id,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences);
+			$conflicts=detect_conflicts(get_member(),$id,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences);
 			$_description=is_null($conflicts)?paragraph(do_lang_tempcode('SUCCESS')):$conflicts;
 
 			regenerate_event_reminder_jobs($id);
 		} else $_description=do_lang_tempcode('SUCCESS');
 
 		$this->donext_type=$type;
-		$this->donext_date=strval($start_year).'-'.strval($start_month).'-'.strval($start_day);
+		$start_day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+		$this->donext_date=strval($start_year).'-'.strval($start_month).'-'.strval($start_day_of_month);
 
 		return $_description;
 	}
diff --git a/cms/pages/modules/cms_news.php b/cms/pages/modules/cms_news.php
index dec4aac..69e6654 100644
--- a/cms/pages/modules/cms_news.php
+++ b/cms/pages/modules/cms_news.php
@@ -438,7 +438,7 @@ class Module_cms_news extends standard_aed_module
 			$start_hour=post_param_integer('schedule_hour');
 			$start_minute=post_param_integer('schedule_minute');
 			require_code('calendar2');
-			$event_id=add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('PUBLISH_NEWS',$title),$schedule_code,3,0,$start_year,$start_month,$start_day,$start_hour,$start_minute);
+			$event_id=add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('PUBLISH_NEWS',$title),$schedule_code,3,0,$start_year,$start_month,$start_day,'day_of_month',$start_hour,$start_minute);
 			regenerate_event_reminder_jobs($event_id,true);
 		}
 
@@ -513,7 +513,7 @@ class Module_cms_news extends standard_aed_module
 				$start_day=post_param_integer('schedule_day');
 				$start_hour=post_param_integer('schedule_hour');
 				$start_minute=post_param_integer('schedule_minute');
-				$event_id=add_calendar_event(db_get_first_id(),'none',NULL,0,do_lang('PUBLISH_NEWS',0,post_param('title')),$schedule_code,3,0,$start_year,$start_month,$start_day,$start_hour,$start_minute);
+				$event_id=add_calendar_event(db_get_first_id(),'none',NULL,0,do_lang('PUBLISH_NEWS',0,post_param('title')),$schedule_code,3,0,$start_year,$start_month,$start_day,'day_of_month',$start_hour,$start_minute);
 				regenerate_event_reminder_jobs($event_id,true);
 			}
 		}
diff --git a/forum/pages/modules/topics.php b/forum/pages/modules/topics.php
index c36f6ea..1fd1772 100644
--- a/forum/pages/modules/topics.php
+++ b/forum/pages/modules/topics.php
@@ -2015,7 +2015,7 @@ END;
 					$start_hour=post_param_integer('schedule_hour');
 					$start_minute=post_param_integer('schedule_minute');
 					require_code('calendar2');
-					$event_id=add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('ADD_POST'),$schedule_code,3,0,$start_year,$start_month,$start_day,$start_hour,$start_minute);
+					$event_id=add_calendar_event(db_get_first_id(),'',NULL,0,do_lang('ADD_POST'),$schedule_code,3,0,$start_year,$start_month,$start_day,'day_of_month',$start_hour,$start_minute);
 					regenerate_event_reminder_jobs($event_id);
 
 					$text=do_lang_tempcode('SUCCESS');
diff --git a/lang/EN/calendar.ini b/lang/EN/calendar.ini
index f960fb1..2233bc4 100644
--- a/lang/EN/calendar.ini
+++ b/lang/EN/calendar.ini
@@ -143,3 +143,16 @@ NOTIFICATION_TYPE_calendar_reminder=Reminders for calendar events subscribed to
 NOTIFICATION_TYPE_calendar_event=New calendar event added
 CALENDAR_EVENT_NOTIFICATION_MAIL_SUBJECT=New calendar event, {2}
 CALENDAR_EVENT_NOTIFICATION_MAIL=A new calendar event, {2}, has been added to {1}. You can view it from the following URL:\n{3}
+
+MONTHLY_SPEC_TYPE=Monthly recurrence
+DESCRIPTION_MONTHLY_SPEC_TYPE=You have advanced control over how exactly each monthly occurrence should be encoded. The possibilities have automatically been detected based upon the initial date and time you specified.
+
+CALENDAR_MONTHLY_RECURRENCE_day_of_month=Every n<sup>th</sup> day from start of month (i.e. 1&ndash;31)
+CALENDAR_MONTHLY_RECURRENCE_day_of_month_backwards=Every n<sup>th</sup> day going back from end of month (i.e. 31&ndash;1)
+CALENDAR_MONTHLY_RECURRENCE_dow_of_month=Every n<sup>th</sup> of some day of the week from start of month
+CALENDAR_MONTHLY_RECURRENCE_dow_of_month_backwards=Every n<sup>th</sup> of some day of the week going back from end of month
+
+CALENDAR_MONTHLY_RECURRENCE_CONCRETE_day_of_month=Every {1} of month
+CALENDAR_MONTHLY_RECURRENCE_CONCRETE_day_of_month_backwards=Every {1} day going back from end of month
+CALENDAR_MONTHLY_RECURRENCE_CONCRETE_dow_of_month=Every {1} {2} of month
+CALENDAR_MONTHLY_RECURRENCE_CONCRETE_dow_of_month_backwards=Every {1} {2} going back from end of month
diff --git a/site/pages/modules/calendar.php b/site/pages/modules/calendar.php
index de3ff93..4d6771d 100644
--- a/site/pages/modules/calendar.php
+++ b/site/pages/modules/calendar.php
@@ -43,7 +43,7 @@ class Module_calendar
 		$info['organisation']='ocProducts';
 		$info['hacked_by']=NULL;
 		$info['hack_version']=NULL;
-		$info['version']=6;
+		$info['version']=7;
 		$info['locked']=false;
 		$info['update_require_upgrade']=1;
 		return $info;
@@ -106,11 +106,13 @@ class Module_calendar
 				'e_start_year'=>'INTEGER',
 				'e_start_month'=>'INTEGER',
 				'e_start_day'=>'INTEGER',
+				'e_start_monthly_spec_type'=>'ID_TEXT', // day_of_month|day_of_month_backwards|dow_of_month|dow_of_month_backwards
 				'e_start_hour'=>'?INTEGER',
 				'e_start_minute'=>'?INTEGER',
 				'e_end_year'=>'?INTEGER',
 				'e_end_month'=>'?INTEGER',
 				'e_end_day'=>'?INTEGER',
+				'e_end_monthly_spec_type'=>'ID_TEXT', // day_of_month|day_of_month_backwards|dow_of_month|dow_of_month_backwards
 				'e_end_hour'=>'?INTEGER',
 				'e_end_minute'=>'?INTEGER',
 				'e_timezone'=>'ID_TEXT', // The settings above are stored in GMT, were converted from this timezone, and back to this timezone if e_do_timezone_conv==1
@@ -122,7 +124,7 @@ class Module_calendar
 				'allow_trackbacks'=>'BINARY',
 				'notes'=>'LONG_TEXT',
 				'e_type'=>'AUTO_LINK',
-				'validated'=>'BINARY'
+				'validated'=>'BINARY',
 			));
 	
 			$GLOBALS['SITE_DB']->create_index('calendar_events','e_views',array('e_views'));
@@ -239,6 +241,12 @@ class Module_calendar
 				}
 			}
 		}
+
+		if ((!is_null($upgrade_from)) && ($upgrade_from<7))
+		{
+			$GLOBALS['SITE_DB']->add_table_field('calendar_events','e_start_monthly_spec_type','ID_TEXT','day_of_month');
+			$GLOBALS['SITE_DB']->add_table_field('calendar_events','e_end_monthly_spec_type','ID_TEXT','day_of_month');
+		}
 	}
 
 	/**
@@ -1357,7 +1365,8 @@ class Module_calendar
 			}
 		}
 
-		$__first_date=mktime($event['e_start_hour'],$event['e_start_minute'],0,$event['e_start_month'],$event['e_start_day'],$event['e_start_year']);
+		$start_day_of_month=find_concrete_day_of_month($event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type']);
+		$__first_date=mktime($event['e_start_hour'],$event['e_start_minute'],0,$event['e_start_month'],$start_day_of_month,$event['e_start_year']);
 		$_first_date=cal_utctime_to_usertime(
 			$__first_date,
 			$event['e_timezone'],
@@ -1417,7 +1426,7 @@ class Module_calendar
 				{
 					if (is_null($event['e_end_year']) || is_null($event['e_end_month']) || is_null($event['e_end_day']))
 					{
-						$event['e_end_day']=$event['e_start_day'];
+						$event['e_end_day']=$start_day_of_month;
 						$event['e_end_month']=$event['e_start_month'];
 						$event['e_end_year']=$event['e_start_year'];
 					}
@@ -1426,19 +1435,30 @@ class Module_calendar
 				{
 					$event['e_end_year']+=intval($explode[0])-$event['e_start_year'];
 					$event['e_end_month']+=intval($explode[1])-$event['e_start_month'];
-					$event['e_end_day']+=intval($explode[2])-$event['e_start_day'];
+					if ($event['e_start_monthly_spec_type']!='day_of_month')
+					{
+						$event['e_end_day']=find_concrete_day_of_month($event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type']);
+					} else
+					{
+						$event['e_end_day']+=intval($explode[2])-$event['e_start_day'];
+					}
 				}
 				$event['e_start_year']=intval($explode[0]);
 				$event['e_start_month']=intval($explode[1]);
 				$event['e_start_day']=intval($explode[2]);
+
+				// Been 'fixed' at this point
+				$event['e_start_monthly_spec_type']='day_of_month';
+				$event['e_end_monthly_spec_type']='day_of_month';
+				$event['e_start_day']=$start_day_of_month;
 			}
 		}
-		$time_raw=cal_get_start_utctime_for_event($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_hour'],$event['e_start_minute'],$event['e_do_timezone_conv']==1);
+		$time_raw=cal_get_start_utctime_for_event($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],$event['e_start_hour'],$event['e_start_minute'],$event['e_do_timezone_conv']==1);
 		$from=cal_utctime_to_usertime($time_raw,$event['e_timezone'],$event['e_do_timezone_conv']==1);
 		$day_formatted=locale_filter(date(do_lang('calendar_date'),$from));
 		if (!is_null($event['e_end_year']) && !is_null($event['e_end_month']) && !is_null($event['e_end_day']))
 		{
-			$to_raw=cal_get_end_utctime_for_event($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_hour'],$event['e_end_minute'],$event['e_do_timezone_conv']==1);
+			$to_raw=cal_get_end_utctime_for_event($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],$event['e_end_hour'],$event['e_end_minute'],$event['e_do_timezone_conv']==1);
 			$to=cal_utctime_to_usertime($to_raw,$event['e_timezone'],$event['e_do_timezone_conv']==1);
 			$to_day_formatted=locale_filter(date(do_lang('calendar_date'),$to));
 			$time2=date_range($from,$to,!is_null($event['e_start_hour']));
@@ -1589,12 +1609,13 @@ class Module_calendar
 
 		if ((has_actual_page_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar')) && (has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(),'calendar',strval($event['e_type']))))
 		{
-			$_from=cal_get_start_utctime_for_event($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_hour'],$event['e_start_minute'],$event['e_do_timezone_conv']==1);
+			$start_day_of_month=find_concrete_day_of_month($event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type']);
+			$_from=cal_get_start_utctime_for_event($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],$event['e_start_hour'],$event['e_start_minute'],$event['e_do_timezone_conv']==1);
 			$from=cal_utctime_to_usertime($_from,$event['e_timezone'],$event['e_do_timezone_conv']==1);
 			$to=mixed();
 			if (!is_null($event['e_end_year']) && !is_null($event['e_end_month']) && !is_null($event['e_end_day']))
 			{
-				$_to=cal_get_end_utctime_for_event($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_hour'],$event['e_end_minute'],$event['e_do_timezone_conv']==1);
+				$_to=cal_get_end_utctime_for_event($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],$event['e_end_hour'],$event['e_end_minute'],$event['e_do_timezone_conv']==1);
 				$to=cal_utctime_to_usertime($_to,$event['e_timezone'],$event['e_do_timezone_conv']==1);
 			}
 
@@ -1602,7 +1623,7 @@ class Module_calendar
 		}
 
 		// Add next reminder to job system
-		$recurrences=find_periods_recurrence($event['e_timezone'],1,$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],is_null($event['e_start_hour'])?0:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],is_null($event['e_end_hour'])?0:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],$event['e_recurrence'],min(1,$event['e_recurrences']));
+		$recurrences=find_periods_recurrence($event['e_timezone'],1,$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],is_null($event['e_start_hour'])?0:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],is_null($event['e_end_hour'])?0:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],$event['e_recurrence'],min(1,$event['e_recurrences']));
 		if (array_key_exists(0,$recurrences))
 		{
 			$GLOBALS['SITE_DB']->query_insert('calendar_jobs',array(
diff --git a/sources/calendar.php b/sources/calendar.php
index 1172f64..1b121ec 100644
--- a/sources/calendar.php
+++ b/sources/calendar.php
@@ -77,11 +77,15 @@ function date_from_week_of_year($year,$week)
  * @param  integer		The year the event starts at. This and the below are in server time
  * @param  integer		The month the event starts at
  * @param  integer		The day the event starts at
+ * @param  ID_TEXT		In-month specification type for start date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  integer		The hour the event starts at
  * @param  integer		The minute the event starts at
  * @param  ?integer		The year the event ends at (NULL: not a multi day event)
  * @param  ?integer		The month the event ends at (NULL: not a multi day event)
  * @param  ?integer		The day the event ends at (NULL: not a multi day event)
+ * @param  ID_TEXT		In-month specification type for end date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer		The hour the event ends at (NULL: not a multi day event / all day event)
  * @param  ?integer		The minute the event ends at (NULL: not a multi day event / all day event)
  * @param  string			The event recurrence
@@ -90,7 +94,7 @@ function date_from_week_of_year($year,$week)
  * @param  ?TIME			The timestamp that found times must not exceed. In user-time (NULL: 20 years time)
  * @return array			A list of pairs for period times (timestamps, in user-time). Actually a series of pairs, 'window-bound timestamps' is first pair, then 'true coverage timestamps', then 'true coverage timestamps without timezone conversions'
  */
-function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences,$period_start=NULL,$period_end=NULL)
+function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences,$period_start=NULL,$period_end=NULL)
 {
 	if ($recurrences===0) return array();
 
@@ -116,34 +120,47 @@ function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_
 	$dif_day=0;
 	$dif_month=0;
 	$dif_year=0;
-	$dif=utctime_to_usertime()-utctime_to_usertime(mktime($start_hour,$start_minute,0,$start_month,$start_day,$start_year));
+	$day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+	$dif=utctime_to_usertime()-utctime_to_usertime(mktime($start_hour,$start_minute,0,$start_month,$day_of_month,$start_year));
+	$start_day_of_month=$start_day;
 	switch ($recurrence) // If a long way out of range, accelerate forward before steadedly looping forward till we might find a match (doesn't jump fully forward, due to possibility of timezones complicating things)
 	{
 		case 'daily':
 			$dif_day=1;
 			if ($dif>60*60*24*10)
+			{
 				$start_day+=$dif_day*intval(floor(floatval($dif)/(60.0*60.0*24.0)));
+			}
 			break;
 		case 'weekly':
 			$dif_day=7;
 			if ($dif>60*60*24*70)
+			{
 				$start_day+=$dif_day*intval(floor(floatval($dif)/(60.0*60.0*24.0)))-70;
+			}
 			break;
 		case 'monthly':
 			$dif_month=1;
 			if ($dif>60*60*24*31*10)
+			{
 				$start_month+=$dif_month*intval(floor(floatval($dif)/(60.0*60.0*24.0*31.0)))-10;
+				$start_day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+			}
 			break;
 		case 'yearly':
 			$dif_year=1;
 			if ($dif>60*60*24*365*10)
+			{
 				$start_year+=$dif_year*intval(floor(floatval($dif)/(60.0*60.0*24.0*365.0)))-1;
+			}
 			break;
 	}
 
 	$_b=mixed();
 	$b=mixed();
 
+	$no_end=false;
+
 	do
 	{
 		/*
@@ -156,7 +173,7 @@ function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_
 		The server already has the day stored UTC which may be different to the day stored for the +1 timezone (in fact either the start or end day will be stored differently, assuming there is an end day)
 		*/
 
-		$_a=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_hour,$start_minute,$do_timezone_conv==1);
+		$_a=cal_get_start_utctime_for_event($timezone,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$do_timezone_conv==1);
 		$a=cal_utctime_to_usertime(
 			$_a,
 			$timezone,
@@ -164,9 +181,10 @@ function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_
 		);
 		if ((is_null($start_hour)) && (is_null($end_year) || is_null($end_month) || is_null($end_day))) // All day event with no end date, should be same as start date
 		{
-			$end_day=$start_day;
+			$end_day=$start_day_of_month;
 			$end_month=$start_month;
 			$end_year=$start_year;
+			$no_end=true;
 		}
 		if (is_null($end_year) || is_null($end_month) || is_null($end_day))
 		{
@@ -174,7 +192,7 @@ function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_
 			$b=NULL;
 		} else
 		{
-			$_b=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_hour,$end_minute,$do_timezone_conv==1);
+			$_b=cal_get_end_utctime_for_event($timezone,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$do_timezone_conv==1);
 			$b=cal_utctime_to_usertime(
 				$_b,
 				$timezone,
@@ -190,16 +208,36 @@ function find_periods_recurrence($timezone,$do_timezone_conv,$start_year,$start_
 		}
 		$i++;
 
-		$start_day+=$dif_day;
-		$start_month+=$dif_month;
 		$start_year+=$dif_year;
+		$start_month+=$dif_month;
+		if ($start_monthly_spec_type=='day_of_month')
+		{
+			$start_day+=$dif_day;
+		} else
+		{
+			$start_day_of_month=find_concrete_day_of_month($start_year,$start_month,$start_day,$start_monthly_spec_type);
+		}
 		if (!is_null($end_year) && !is_null($end_month) && !is_null($end_day))
 		{
-			$end_day+=$dif_day;
-			$end_month+=$dif_month;
 			$end_year+=$dif_year;
+			$end_month+=$dif_month;
+			if ($end_monthly_spec_type=='day_of_month')
+			{
+				$end_day+=$dif_day;
+			} else
+			{
+				$end_day_of_month=find_concrete_day_of_month($end_year,$end_month,$end_day,$end_monthly_spec_type);
+			}
+		}
+
+		// Let it reset
+		if ($no_end)
+		{
+			$end_day=NULL;
+			$end_month=NULL;
+			$end_year=NULL;
 		}
-		
+
 		if ($i==300) break; // Let's be reasonable
 	}
 	while (($recurrence!='') && ($recurrence!='none') && ($a<$period_end) && ((is_null($recurrences)) || ($i<$recurrences)));
@@ -248,7 +286,7 @@ function regenerate_event_reminder_jobs($id,$force=false)
 	$GLOBALS['SITE_DB']->query_delete('calendar_jobs',array('j_event_id'=>$id));
 
 	$period_start=$force?0:NULL;
-	$recurrences=find_periods_recurrence($event['e_timezone'],$event['e_do_timezone_conv'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],is_null($event['e_start_hour'])?0:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],is_null($event['e_end_hour'])?23:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],$event['e_recurrence'],min(1,$event['e_recurrences']),$period_start);
+	$recurrences=find_periods_recurrence($event['e_timezone'],$event['e_do_timezone_conv'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],is_null($event['e_start_hour'])?0:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],is_null($event['e_end_hour'])?23:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],$event['e_recurrence'],min(1,$event['e_recurrences']),$period_start);
 	if ((array_key_exists(0,$recurrences)) && ($recurrences[0][0]==$recurrences[0][2]/*really starts in window, not just spanning it*/))
 	{
 		if ($event['e_type']==db_get_first_id()) // Add system command job if necessary
@@ -419,10 +457,10 @@ function calendar_matches($member_id,$restrict,$period_start,$period_end,$filter
 					}
 					if ($key!=0)
 					{
-						list($full_url,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)=get_event_data_ical($calendar_nodes[$key]);
+						list($full_url,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)=get_event_data_ical($calendar_nodes[$key]);
 						$is_public=1;
 
-						$event=array('e_recurrence'=>$recurrence,'e_content'=>$content,'e_title'=>$title,'e_id'=>$feed_url,'e_priority'=>$priority,'t_logo'=>'calendar/rss','e_recurrences'=>$recurrences,'e_seg_recurrences'=>$seg_recurrences,'e_is_public'=>$is_public,'e_start_year'=>$start_year,'e_start_month'=>$start_month,'e_start_day'=>$start_day,'e_start_hour'=>$start_hour,'e_start_minute'=>$start_minute,'e_end_year'=>$end_year,'e_end_month'=>$end_month,'e_end_day'=>$end_day,'e_end_hour'=>$end_hour,'e_end_minute'=>$end_minute,'e_timezone'=>$timezone);
+						$event=array('e_recurrence'=>$recurrence,'e_content'=>$content,'e_title'=>$title,'e_id'=>$feed_url,'e_priority'=>$priority,'t_logo'=>'calendar/rss','e_recurrences'=>$recurrences,'e_seg_recurrences'=>$seg_recurrences,'e_is_public'=>$is_public,'e_start_year'=>$start_year,'e_start_month'=>$start_month,'e_start_day'=>$start_day,'e_start_hour'=>$start_hour,'e_start_minute'=>$start_minute,'e_end_year'=>$end_year,'e_end_month'=>$end_month,'e_end_day'=>$end_day,'e_end_hour'=>$end_hour,'e_end_minute'=>$end_minute,'e_timezone'=>$timezone,'e_start_monthly_spec_type'=>'day_of_month','e_end_monthly_spec_type'=>'day_of_month');
 						if (!is_null($event_type)) $event['t_logo']=$_event_types[$event_type]['t_logo'];
 						if (!is_null($type))
 						{
@@ -431,7 +469,7 @@ function calendar_matches($member_id,$restrict,$period_start,$period_end,$filter
 								$event['t_logo']=$event_types[$type];
 						}
 
-						$their_times=find_periods_recurrence($timezone,0,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences,$period_start,$period_end);
+						$their_times=find_periods_recurrence($timezone,0,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences,$period_start,$period_end);
 
 						// Now search every combination to see if we can get a hit
 						foreach ($their_times as $their)
@@ -466,7 +504,7 @@ function calendar_matches($member_id,$restrict,$period_start,$period_end,$filter
 						$from=utctime_to_usertime($item['clean_add_date']);
 						if (($from>=$period_start) && ($from<$period_end))
 						{
-							$event+=array('e_start_year'=>date('Y',$from),'e_start_month'=>date('m',$from),'e_start_day'=>date('D',$from),'e_start_hour'=>date('H',$from),'e_start_minute'=>date('i',$from),'e_end_year'=>NULL,'e_end_month'=>NULL,'e_end_day'=>NULL,'e_end_hour'=>NULL,'e_end_minute'=>NULL);
+							$event+=array('e_start_year'=>intval(date('Y',$from)),'e_start_month'=>intval(date('m',$from)),'e_start_day'=>intval(date('D',$from)),'e_start_hour'=>intval(date('H',$from)),'e_start_minute'=>intval(date('i',$from)),'e_end_year'=>NULL,'e_end_month'=>NULL,'e_end_day'=>NULL,'e_end_hour'=>NULL,'e_end_minute'=>NULL,'e_start_monthly_spec_type'=>'day_of_month','e_end_monthly_spec_type'=>'day_of_month');
 							$matches[]=array($full_url,$event,$from,NULL,$from,NULL,$from,NULL);
 						}
 					}
@@ -492,7 +530,7 @@ function calendar_matches($member_id,$restrict,$period_start,$period_end,$filter
 	{
 		if (!has_category_access(get_member(),'calendar',strval($event['e_type']))) continue;
 
-		$their_times=find_periods_recurrence($event['e_timezone'],$event['e_do_timezone_conv'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_hour'],$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_hour'],$event['e_end_minute'],$event['e_recurrence'],$event['e_recurrences'],$period_start,$period_end);
+		$their_times=find_periods_recurrence($event['e_timezone'],$event['e_do_timezone_conv'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type'],$event['e_start_hour'],$event['e_start_minute'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type'],$event['e_end_hour'],$event['e_end_minute'],$event['e_recurrence'],$event['e_recurrences'],$period_start,$period_end);
 
 		// Now search every combination to see if we can get a hit
 		foreach ($their_times as $their)
@@ -542,20 +580,24 @@ function nice_get_events($only_owned,$it,$edit_viewable_events=true)
  * @param  ?integer		The year the event starts at. This and the below are in server time (NULL: default)
  * @param  ?integer		The month the event starts at (NULL: default)
  * @param  ?integer		The day the event starts at (NULL: default)
+ * @param  ID_TEXT		In-month specification type for start date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer		The hour the event starts at (NULL: default)
  * @param  ?integer		The minute the event starts at (NULL: default)
  * @param  ?integer		The year the event ends at (NULL: not a multi day event)
  * @param  ?integer		The month the event ends at (NULL: not a multi day event)
  * @param  ?integer		The day the event ends at (NULL: not a multi day event)
+ * @param  ID_TEXT		In-month specification type for end date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer		The hour the event ends at (NULL: not a multi day event)
  * @param  ?integer		The minute the event ends at (NULL: not a multi day event)
  * @param  string			The event recurrence
  * @param  ?integer		The number of recurrences (NULL: none/infinite)
  * @return ?tempcode		Information about conflicts (NULL: none)
  */
-function detect_conflicts($member_id,$skip_id,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences)
+function detect_conflicts($member_id,$skip_id,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences)
 {
-	$our_times=find_periods_recurrence(get_users_timezone(),1,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$recurrence,$recurrences);
+	$our_times=find_periods_recurrence(get_users_timezone(),1,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$recurrence,$recurrences);
 
 	$conflicts=detect_happening_at($member_id,$skip_id,$our_times,!has_specific_permission(get_member(),'sense_personal_conflicts'));
 
@@ -585,10 +627,13 @@ function detect_conflicts($member_id,$skip_id,$start_year,$start_month,$start_da
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @return integer			Hour
  */
-function find_timezone_start_hour_in_utc($timezone,$year,$month,$day)
+function find_timezone_start_hour_in_utc($timezone,$year,$month,$day,$monthly_spec_type)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
 	$t1=mktime(0,0,0,$month,$day,$year);
 	$t2=tz_time($t1,$timezone);
 	$t2-=2*($t2-$t1);
@@ -603,10 +648,13 @@ function find_timezone_start_hour_in_utc($timezone,$year,$month,$day)
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @return integer			Hour
  */
-function find_timezone_start_minute_in_utc($timezone,$year,$month,$day)
+function find_timezone_start_minute_in_utc($timezone,$year,$month,$day,$monthly_spec_type)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
 	$t1=mktime(0,0,0,$month,$day,$year);
 	$t2=tz_time($t1,$timezone);
 	$t2-=2*($t2-$t1);
@@ -621,10 +669,13 @@ function find_timezone_start_minute_in_utc($timezone,$year,$month,$day)
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @return integer			Hour
  */
-function find_timezone_end_hour_in_utc($timezone,$year,$month,$day)
+function find_timezone_end_hour_in_utc($timezone,$year,$month,$day,$monthly_spec_type)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
 	$t1=mktime(23,59,0,$month,$day,$year);
 	$t2=tz_time($t1,$timezone);
 	$t2-=2*($t2-$t1);
@@ -639,10 +690,13 @@ function find_timezone_end_hour_in_utc($timezone,$year,$month,$day)
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @return integer			Hour
  */
-function find_timezone_end_minute_in_utc($timezone,$year,$month,$day)
+function find_timezone_end_minute_in_utc($timezone,$year,$month,$day,$monthly_spec_type)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
 	$t1=mktime(23,59,0,$month,$day,$year);
 	$t2=tz_time($t1,$timezone);
 	$t2-=2*($t2-$t1);
@@ -657,13 +711,17 @@ function find_timezone_end_minute_in_utc($timezone,$year,$month,$day)
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer			Hour (NULL: start hour of day in the timezone expressed as UTC, for whatever day the given midnight day/month/year shifts to after timezone conversion)
  * @param  ?integer			Minute (NULL: start minute of day in the timezone expressed as UTC, for whatever day the given midnight day/month/year shifts to after timezone conversion)
  * @param  boolean			Whether the time should be converted to the viewer's own timezone instead.
  * @return TIME				Timestamp
  */
-function cal_get_start_utctime_for_event($timezone,$year,$month,$day,$hour,$minute,$show_in_users_timezone)
+function cal_get_start_utctime_for_event($timezone,$year,$month,$day,$monthly_spec_type,$hour,$minute,$show_in_users_timezone)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
+
 	$_hour=is_null($hour)?0:$hour;
 	$_minute=is_null($minute)?0:$minute;
 
@@ -718,13 +776,17 @@ function cal_get_start_utctime_for_event($timezone,$year,$month,$day,$hour,$minu
  * @param  integer			Year
  * @param  integer			Month
  * @param  integer			Day
+ * @param  ID_TEXT			In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer			Hour (NULL: end hour of day in the timezone expressed as UTC, for whatever day the given midnight day/month/year shifts to after timezone conversion)
  * @param  ?integer			Minute (NULL: end minute of day in the timezone expressed as UTC, for whatever day the given midnight day/month/year shifts to after timezone conversion)
  * @param  boolean			Whether the time should be converted to the viewer's own timezone instead.
  * @return TIME				Timestamp
  */
-function cal_get_end_utctime_for_event($timezone,$year,$month,$day,$hour,$minute,$show_in_users_timezone)
+function cal_get_end_utctime_for_event($timezone,$year,$month,$day,$monthly_spec_type,$hour,$minute,$show_in_users_timezone)
 {
+	$day=find_concrete_day_of_month($year,$month,$day,$monthly_spec_type);
+
 	$_hour=is_null($hour)?23:$hour;
 	$_minute=is_null($minute)?59:$minute;
 
@@ -824,13 +886,15 @@ function detect_happening_at($member_id,$skip_id,$our_times,$restrict=true,$peri
 			$event['e_start_year'],
 			$event['e_start_month'],
 			$event['e_start_day'],
-			is_null($event['e_start_hour'])?find_timezone_start_hour_in_utc($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day']):$event['e_start_hour'],
-			is_null($event['e_start_minute'])?find_timezone_start_minute_in_utc($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day']):$event['e_start_minute'],
+			$event['e_start_monthly_spec_type'],
+			is_null($event['e_start_hour'])?find_timezone_start_hour_in_utc($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type']):$event['e_start_hour'],
+			is_null($event['e_start_minute'])?find_timezone_start_minute_in_utc($event['e_timezone'],$event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type']):$event['e_start_minute'],
 			$event['e_end_year'],
 			$event['e_end_month'],
 			$event['e_end_day'],
-			is_null($event['e_end_hour'])?find_timezone_end_hour_in_utc($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day']):$event['e_end_hour'],
-			is_null($event['e_end_minute'])?find_timezone_end_minute_in_utc($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day']):$event['e_end_minute'],
+			$event['e_end_monthly_spec_type'],
+			is_null($event['e_end_hour'])?find_timezone_end_hour_in_utc($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type']):$event['e_end_hour'],
+			is_null($event['e_end_minute'])?find_timezone_end_minute_in_utc($event['e_timezone'],$event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type']):$event['e_end_minute'],
 			$event['e_recurrence'],
 			$event['e_recurrences'],
 			$period_start,
@@ -876,3 +940,128 @@ function detect_happening_at($member_id,$skip_id,$our_times,$restrict=true,$peri
 
 	return $conflicts;
 }
+
+/**
+ * Given a specially encoded day of month, work out the real day of the month.
+ *
+ * @param  integer		The concrete year
+ * @param  integer		The concrete month
+ * @param  integer		The encoded day of month
+ * @param  ID_TEXT		In-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
+ * @return integer		Concrete day
+ */
+function find_concrete_day_of_month($year,$month,$day,$monthly_spec_type)
+{
+	switch ($monthly_spec_type)
+	{
+		case 'day_of_month':
+		default:
+			$day_of_month=$day;
+			break;
+		case 'day_of_month_backwards':
+			$day_of_month=intval(date('d',mktime(0,0,0,$month+1,0,$year)))-$day+1;
+			break;
+		case 'dow_of_month':
+			$days=array('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday');
+			$month_start=mktime(0,0,0,$month,1,$year);
+			$timestamp=strtotime('+'.strval(intval(1.0+floatval($day)/7.0)).' '.strval($days[$day%7]),$month_start);
+			$day_of_month=intval(date('d',$timestamp));
+			break;
+		case 'dow_of_month_backwards':
+			$days=array('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday');
+			$month_end=mktime(0,0,0,$month+1,0,$year);
+			$timestamp=strtotime('-'.strval(intval(1.0+floatval($day)/7.0)).' '.strval($days[$day%7]),$month_end);
+			$day_of_month=intval(date('d',$timestamp));
+			break;
+	}
+	return $day_of_month;
+}
+
+/**
+ * Given a calendar day of month, work out the day of the month within the specified encoding.
+ *
+ * @param  integer		The concrete year
+ * @param  integer		The concrete month
+ * @param  integer		The encoded day of month
+ * @param  ID_TEXT		In-month specification type
+ * @return integer		Concrete day
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
+ */
+function find_abstract_day($year,$month,$day_of_month,$monthly_spec_type)
+{
+	switch ($monthly_spec_type)
+	{
+		case 'day_of_month':
+		default:
+			$day=$day_of_month;
+			break;
+		case 'day_of_month_backwards':
+			$day=intval(date('d',mktime(0,0,0,$month+1,0,$year)))-$day_of_month+1;
+			break;
+		case 'dow_of_month':
+			$day_code=intval(date('w',mktime(0,0,0,$month,$day_of_month,$year)));
+
+			// Monday is 0 in my mind, not Sunday
+			$day_code--;
+			if ($day_code==-1) $day_code=6;
+
+			$day=$day_code+7*intval(floatval($day_of_month)/7.0);
+			break;
+		case 'dow_of_month_backwards':
+			$day_code=intval(date('w',mktime(0,0,0,$month,$day_of_month,$year)));
+
+			// Monday is 0 in my mind, not Sunday
+			$day_code--;
+			if ($day_code==-1) $day_code=6;
+
+			$month_end=mktime(0,0,0,$month+1,0,$year);
+			$days_in_month=intval(date('d',$month_end));
+
+			$day=$day_code+7*intval(floatval($days_in_month-$day_of_month)/7.0);
+			break;
+	}
+	return $day;
+}
+
+/**
+ * Choose how a recurring monthly event should be encoded.
+ *
+ * @param  integer		The concrete day
+ * @param  integer		The concrete month
+ * @param  integer		The concrete year
+ * @param  ID_TEXT		Current in-month specification type
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
+ * @return tempcode		Chooser
+ */
+function monthly_spec_type_chooser($day_of_month,$month,$year,$default_monthly_spec_type='day_of_month')
+{
+	require_code('form_templates');
+	require_lang('calendar');
+
+	$radios=new ocp_tempcode();
+
+	foreach (array('day_of_month','day_of_month_backwards','dow_of_month','dow_of_month_backwards') as $monthly_spec_type)
+	{
+		$day=find_abstract_day($year,$month,$day_of_month,$monthly_spec_type);
+
+		$timestamp=mktime(0,0,0,$month,$day_of_month,$year);
+
+		if (substr($monthly_spec_type,0,4)=='dow_')
+		{
+			$nth=locale_filter(date('jS',mktime(0,0,0,1,intval(floatval($day)/7.0)+1,$year))); // Bit of a hack. Uses the date locales nth stuff, even when it's not actually a day-of-month here.
+		} else
+		{
+			$nth=locale_filter(date('jS',mktime(0,0,0,$month,$day,$year))); // Bit of a hack. Uses the date locales nth stuff, even when it's not actually a day-of-month here.
+		}
+		$dow=locale_filter(date('l',$timestamp));
+		$month_name=locale_filter(date('M',$timestamp));
+
+		$text=do_lang_tempcode('CALENDAR_MONTHLY_RECURRENCE_CONCRETE_'.$monthly_spec_type,$nth,$dow,$month_name);
+		$description=do_lang_tempcode('CALENDAR_MONTHLY_RECURRENCE_'.$monthly_spec_type);
+
+		$radios->attach(form_input_radio_entry('monthly_spec_type',$monthly_spec_type,$monthly_spec_type==$default_monthly_spec_type,$text,NULL,$description));
+	}
+
+	return form_input_radio(do_lang_tempcode('MONTHLY_SPEC_TYPE'),do_lang_tempcode('DESCRIPTION_MONTHLY_SPEC_TYPE'),$radios,true);
+}
diff --git a/sources/calendar2.php b/sources/calendar2.php
index 1013f7d..eeacc5e 100644
--- a/sources/calendar2.php
+++ b/sources/calendar2.php
@@ -30,14 +30,18 @@
  * @param  integer			The priority
  * @range  1 5
  * @param  BINARY				Whether it is a public event
- * @param  ?integer			The year the event starts at (NULL: default)
- * @param  ?integer			The month the event starts at (NULL: default)
- * @param  ?integer			The day the event starts at (NULL: default)
- * @param  ?integer			The hour the event starts at (NULL: default)
- * @param  ?integer			The minute the event starts at (NULL: default)
+ * @param  integer			The year the event starts at
+ * @param  integer			The month the event starts at
+ * @param  integer			The day the event starts at
+ * @param  ID_TEXT			In-month specification type for start date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
+ * @param  integer			The hour the event starts at
+ * @param  integer			The minute the event starts at
  * @param  ?integer			The year the event ends at (NULL: not a multi day event)
  * @param  ?integer			The month the event ends at (NULL: not a multi day event)
  * @param  ?integer			The day the event ends at (NULL: not a multi day event)
+ * @param  ID_TEXT			In-month specification type for end date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer			The hour the event ends at (NULL: not a multi day event)
  * @param  ?integer			The minute the event ends at (NULL: not a multi day event)
  * @param  ?ID_TEXT			The timezone for the event (NULL: current user's timezone)
@@ -54,7 +58,7 @@
  * @param  ?AUTO_LINK		Force an ID (NULL: don't force an ID)
  * @return AUTO_LINK			The ID of the event
  */
-function add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year=NULL,$end_month=NULL,$end_day=NULL,$end_hour=NULL,$end_minute=NULL,$timezone=NULL,$do_timezone_conv=1,$validated=1,$allow_rating=1,$allow_comments=1,$allow_trackbacks=1,$notes='',$submitter=NULL,$views=0,$add_date=NULL,$edit_date=NULL,$id=NULL)
+function add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year=NULL,$end_month=NULL,$end_day=NULL,$end_monthly_spec_type='day_of_month',$end_hour=NULL,$end_minute=NULL,$timezone=NULL,$do_timezone_conv=1,$validated=1,$allow_rating=1,$allow_comments=1,$allow_trackbacks=1,$notes='',$submitter=NULL,$views=0,$add_date=NULL,$edit_date=NULL,$id=NULL)
 {
 	if (is_null($submitter)) $submitter=get_member();
 	if (is_null($add_date)) $add_date=time();
@@ -78,11 +82,13 @@ function add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$tit
 		'e_start_year'=>$start_year,
 		'e_start_month'=>$start_month,
 		'e_start_day'=>$start_day,
+		'e_start_monthly_spec_type'=>$start_monthly_spec_type,
 		'e_start_hour'=>$start_hour,
 		'e_start_minute'=>$start_minute,
 		'e_end_year'=>$end_year,
 		'e_end_month'=>$end_month,
 		'e_end_day'=>$end_day,
+		'e_end_monthly_spec_type'=>$end_monthly_spec_type,
 		'e_end_hour'=>$end_hour,
 		'e_end_minute'=>$end_minute,
 		'e_timezone'=>$timezone,
@@ -135,14 +141,18 @@ function add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$tit
  * @param  integer			The priority
  * @range  1 5
  * @param  BINARY				Whether it is a public event
- * @param  ?integer			The year the event starts at (NULL: default)
- * @param  ?integer			The month the event starts at (NULL: default)
- * @param  ?integer			The day the event starts at (NULL: default)
- * @param  ?integer			The hour the event starts at (NULL: default)
- * @param  ?integer			The minute the event starts at (NULL: default)
+ * @param  integer			The year the event starts at
+ * @param  integer			The month the event starts at
+ * @param  integer			The day the event starts at
+ * @param  ID_TEXT			In-month specification type for start date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
+ * @param  integer			The hour the event starts at
+ * @param  integer			The minute the event starts at
  * @param  ?integer			The year the event ends at (NULL: not a multi day event)
  * @param  ?integer			The month the event ends at (NULL: not a multi day event)
  * @param  ?integer			The day the event ends at (NULL: not a multi day event)
+ * @param  ID_TEXT			In-month specification type for end date
+ * @set day_of_month day_of_month_backwards dow_of_month dow_of_month_backwards
  * @param  ?integer			The hour the event ends at (NULL: not a multi day event)
  * @param  ?integer			The minute the event ends at (NULL: not a multi day event)
  * @param  ?ID_TEXT			The timezone for the event (NULL: current user's timezone)
@@ -155,7 +165,7 @@ function add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$tit
  * @param  BINARY				Whether the download may be trackbacked
  * @param  LONG_TEXT			Hidden notes pertaining to the download
  */
-function edit_calendar_event($id,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$do_timezone_conv,$meta_keywords,$meta_description,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)
+function edit_calendar_event($id,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$start_monthly_spec_type,$end_hour,$end_minute,$timezone,$do_timezone_conv,$meta_keywords,$meta_description,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)
 {
 	$myrows=$GLOBALS['SITE_DB']->query_select('calendar_events',array('e_title','e_content','e_submitter'),array('id'=>$id),'',1);
 	$myrow=$myrows[0];
@@ -188,11 +198,13 @@ function edit_calendar_event($id,$type,$recurrence,$recurrences,$seg_recurrences
 		'e_start_year'=>$start_year,
 		'e_start_month'=>$start_month,
 		'e_start_day'=>$start_day,
+		'e_start_monthly_spec_type'=>$start_monthly_spec_type,
 		'e_start_hour'=>$start_hour,
 		'e_start_minute'=>$start_minute,
 		'e_end_year'=>$end_year,
 		'e_end_month'=>$end_month,
 		'e_end_day'=>$end_day,
+		'e_end_monthly_spec_type'=>$end_monthly_spec_type,
 		'e_end_hour'=>$end_hour,
 		'e_end_minute'=>$end_minute,
 		'e_timezone'=>$timezone,
diff --git a/sources/calendar_ical.php b/sources/calendar_ical.php
index f6459d6..f687ffd 100644
--- a/sources/calendar_ical.php
+++ b/sources/calendar_ical.php
@@ -48,7 +48,7 @@ function output_ical()
 	if ($filter===0) $filter=NULL;
 	$where='(e_submitter='.strval(get_member()).' OR e_is_public=1)';
 	if (!is_null($filter)) $where.=' AND e_type='.strval($filter);
-	$events=$GLOBALS['SITE_DB']->query('SELECT e_is_public,e_submitter,e_add_date,e_edit_date,e_title,e_content,e_type,validated,id,e_recurrence,e_recurrences,e_start_hour,e_start_minute,e_start_month,e_start_day,e_start_year,e_end_hour,e_end_minute,e_end_month,e_end_day,e_end_year FROM '.get_table_prefix().'calendar_events WHERE '.$where.' ORDER BY e_add_date DESC',10000/*reasonable limit*/);
+
 	echo "BEGIN:VCALENDAR\n";
 	echo "VERSION:2.0\n";
 	echo "PRODID:-//ocProducts/ocPortal//NONSGML v1.0//EN\n";
@@ -67,138 +67,204 @@ function output_ical()
 		echo "X-WR-CALNAME:".ical_escape(get_site_name().": ".$categories[$filter])."\n";
 	}
 
-	foreach ($events as $event)
+	$start=0;
+	do
 	{
-		if (!has_category_access(get_member(),'calendar',strval($event['e_type']))) continue;
-
-		if (($event['e_is_public']==1) || ($event['e_submitter']==get_member()))
+		$events=$GLOBALS['SITE_DB']->query('SELECT * FROM '.get_table_prefix().'calendar_events WHERE '.$where.' ORDER BY e_add_date DESC',1000,$start);
+		foreach ($events as $event)
 		{
-			echo "BEGIN:VEVENT\n";
+			if (!has_category_access(get_member(),'calendar',strval($event['e_type']))) continue;
 
-			echo "DTSTAMP:".date('Ymd',time())."T".date('His',$event['e_add_date'])."\n";
-			echo "CREATED:".date('Ymd',time())."T".date('His',$event['e_add_date'])."\n";
-			if (!is_null($event['e_edit_date'])) echo "LAST-MODIFIED:".date('Ymd',time())."T".date('His',$event['e_edit_date'])."\n";
-
-			echo "SUMMARY:".ical_escape(get_translated_text($event['e_title']))."\n";
-			$description=get_translated_text($event['e_content']);
-			$matches=array();
-			$num_matches=preg_match_all('#\[attachment[^\]]*\](\d+)\[/attachment\]#',$description,$matches);
-			for ($i=0;$i<$num_matches;$i++)
+			if (($event['e_is_public']==1) || ($event['e_submitter']==get_member()))
 			{
-				$description=str_replace($matches[0],'',$description);
-				$attachments=$GLOBALS['SITE_DB']->query_select('attachments',array('*'),array('id'=>intval($matches[1])));
-				if (array_key_exists(0,$attachments))
+				echo "BEGIN:VEVENT\n";
+
+				echo "DTSTAMP:".date('Ymd',time())."T".date('His',$event['e_add_date'])."\n";
+				echo "CREATED:".date('Ymd',time())."T".date('His',$event['e_add_date'])."\n";
+				if (!is_null($event['e_edit_date'])) echo "LAST-MODIFIED:".date('Ymd',time())."T".date('His',$event['e_edit_date'])."\n";
+
+				echo "SUMMARY:".ical_escape(get_translated_text($event['e_title']))."\n";
+				$description=get_translated_text($event['e_content']);
+				$matches=array();
+				$num_matches=preg_match_all('#\[attachment[^\]]*\](\d+)\[/attachment\]#',$description,$matches);
+				for ($i=0;$i<$num_matches;$i++)
 				{
-					$attachment=$attachments[0];
-					require_code('mime_types');
-					echo "ATTACH;FMTTYPE=".ical_escape(get_mime_type($attachment['a_original_filename'])).":".ical_escape(find_script('attachments').'?id='.strval($attachment['id']))."\n";
+					$description=str_replace($matches[0],'',$description);
+					$attachments=$GLOBALS['SITE_DB']->query_select('attachments',array('*'),array('id'=>intval($matches[1])));
+					if (array_key_exists(0,$attachments))
+					{
+						$attachment=$attachments[0];
+						require_code('mime_types');
+						echo "ATTACH;FMTTYPE=".ical_escape(get_mime_type($attachment['a_original_filename'])).":".ical_escape(find_script('attachments').'?id='.strval($attachment['id']))."\n";
+					}
 				}
-			}
-			echo "DESCRIPTION:".ical_escape($description)."\n";
-
-			if (!is_guest($event['e_submitter']))
-				echo "ORGANIZER;CN=".ical_escape($GLOBALS['FORUM_DRIVER']->get_username($event['e_submitter'])).";DIR=".ical_escape($GLOBALS['FORUM_DRIVER']->member_profile_url($event['e_submitter'])).":MAILTO:".ical_escape($GLOBALS['FORUM_DRIVER']->get_member_email_address($event['e_submitter']))."\n";
-			echo "CATEGORIES:".ical_escape($categories[$event['e_type']])."\n";
-			echo "CLASS:".(($event['e_is_public']==1)?'PUBLIC':'PRIVATE')."\n";
-			echo "STATUS:".(($event['validated']==1)?'CONFIRMED':'TENTATIVE')."\n";
-			echo "UID:".ical_escape(strval($event['id']).'@'.get_base_url())."\n";
-			$_url=build_url(array('page'=>'calendar','type'=>'view','id'=>$event['id']),get_module_zone('calendar'),NULL,false,false,true);
-			$url=$_url->evaluate();
-			echo "URL:".ical_escape($url)."\n";
-
-			$forum=get_value('comment_forum__calendar');
-			if (is_null($forum)) $forum=get_option('comments_forum_name');
-			$start=0;
-			do
-			{
-				$count=0;
-				$_comments=$GLOBALS['FORUM_DRIVER']->get_forum_topic_posts($GLOBALS['FORUM_DRIVER']->find_topic_id_for_topic_identifier($forum,'events_'.strval($event['id'])),$count,1000,$start);
-				if (is_array($_comments))
+				echo "DESCRIPTION:".ical_escape($description)."\n";
+
+				if (!is_guest($event['e_submitter']))
+				{
+					echo "ORGANIZER;CN=".ical_escape($GLOBALS['FORUM_DRIVER']->get_username($event['e_submitter'])).";DIR=".ical_escape($GLOBALS['FORUM_DRIVER']->member_profile_url($event['e_submitter']));
+					$addr=$GLOBALS['FORUM_DRIVER']->get_member_email_address($event['e_submitter']);
+					if ($addr!='') echo ":MAILTO:".ical_escape($addr);
+					echo "\n";
+				}
+				echo "CATEGORIES:".ical_escape($categories[$event['e_type']])."\n";
+				echo "CLASS:".(($event['e_is_public']==1)?'PUBLIC':'PRIVATE')."\n";
+				echo "STATUS:".(($event['validated']==1)?'CONFIRMED':'TENTATIVE')."\n";
+				echo "UID:".ical_escape(strval($event['id']).'@'.get_base_url())."\n";
+				$_url=build_url(array('page'=>'calendar','type'=>'view','id'=>$event['id']),get_module_zone('calendar'),NULL,false,false,true);
+				$url=$_url->evaluate();
+				echo "URL:".ical_escape($url)."\n";
+
+				$forum=get_value('comment_forum__calendar');
+				if (is_null($forum)) $forum=get_option('comments_forum_name');
+				$start=0;
+				do
 				{
-					foreach ($_comments as $comment)
+					$count=0;
+					$_comments=$GLOBALS['FORUM_DRIVER']->get_forum_topic_posts($GLOBALS['FORUM_DRIVER']->find_topic_id_for_topic_identifier($forum,'events_'.strval($event['id'])),$count,1000,$start);
+					if (is_array($_comments))
 					{
-						if ($comment['title']!='') $comment['message']=$comment['title'].': '.$comment['message'];
-						echo "COMMENT:".ical_escape($comment['message'].' - '.$GLOBALS['FORUM_DRIVER']->get_username($comment['user']).' ('.get_timezoned_date($comment['date']).')')."\n";
+						foreach ($_comments as $comment)
+						{
+							if ($comment['title']!='') $comment['message']=$comment['title'].': '.$comment['message'];
+							echo "COMMENT:".ical_escape($comment['message'].' - '.$GLOBALS['FORUM_DRIVER']->get_username($comment['user']).' ('.get_timezoned_date($comment['date']).')')."\n";
+						}
 					}
+					$start+=1000;
 				}
-				$start+=1000;
-			}
-			while (count($_comments)==1000);
+				while (count($_comments)==1000);
 
-			$time=mktime(is_null($event['e_start_hour'])?12:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],0,$event['e_start_month'],$event['e_start_day'],$event['e_start_year']);
-			$time2=mixed();
-			$time2=(is_null($event['e_end_year']) || is_null($event['e_end_month']) || is_null($event['e_end_day']))?NULL:mktime(is_null($event['e_end_hour'])?12:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],0,$event['e_end_month'],$event['e_end_day'],$event['e_end_year']);
-			if ($event['e_recurrence']!='none')
-			{
-				$parts=explode(' ',$event['e_recurrence']);
-				if (count($parts)==1)
+				$start_day_of_month=find_concrete_day_of_month($event['e_start_year'],$event['e_start_month'],$event['e_start_day'],$event['e_start_monthly_spec_type']);
+				$time=mktime(is_null($event['e_start_hour'])?12:$event['e_start_hour'],is_null($event['e_start_minute'])?0:$event['e_start_minute'],0,$event['e_start_month'],$start_day_of_month,$event['e_start_year']);
+				if (is_null($event['e_end_year']) || is_null($event['e_end_month']) || is_null($event['e_end_day']))
 				{
-					echo "DTSTART;TZ=".$event['e_timezone'].":".date('Ymd',$time).(is_null($event['e_start_hour'])?"":("T".date('His',$time)))."\n";
-					if (!is_null($time2)) echo "DTEND:".date('Ymd',$time2)."T".(is_null($event['e_end_hour'])?"":("T".date('His',$time2)))."\n";
-					$recurrence_code='FREQ='.strtoupper($parts[0]);
-					echo "RRULE:".$recurrence_code.(is_null($event['e_recurrences'])?'':(";COUNT=".strval($event['e_recurrences'])))."\n";
+					$time2=mixed();
 				} else
 				{
-					for ($i=0;$i<strlen($parts[1]);$i++)
+					$end_day_of_month=find_concrete_day_of_month($event['e_end_year'],$event['e_end_month'],$event['e_end_day'],$event['e_end_monthly_spec_type']);
+					$time2=mktime(is_null($event['e_end_hour'])?12:$event['e_end_hour'],is_null($event['e_end_minute'])?0:$event['e_end_minute'],0,$event['e_end_month'],$end_day_of_month,$event['e_end_year']);
+				}
+				if ($event['e_recurrence']!='none')
+				{
+					$parts=explode(' ',$event['e_recurrence']);
+					if (count($parts)==1)
 					{
-						switch ($parts[0])
+						$parts[]='1';
+					}
+
+					// Recurrence pattern handling
+					for ($i=0;$i<strlen($parts[1]);$i++) // For each part of the recurrence pattern we set out a separate event intervaling in step with it
+					{
+						if ($i!=0)
 						{
-							case 'daily':
-								$time+=60*60*24;
-								if (!is_null($time2)) $time2+=60*60*24;
-								break;
-							case 'weekly':
-								$time+=60*60*24*7;
-								if (!is_null($time2)) $time2+=60*60*24*7;
-								break;
-							case 'monthly':
-								$days_in_month=intval(date('D',mktime(0,0,0,intval(date('m',$time))+1,0,intval(date('Y',$time)))));
-								$time+=60*60*$days_in_month;
-								if (!is_null($time2)) $time2+=60*60*$days_in_month;
-								break;
-							case 'yearly':
-								$days_in_year=intval(date('Y',mktime(0,0,0,0,0,intval(date('Y',$time))+1)));
-								$time+=60*60*24*$days_in_year;
-								if (!is_null($time2)) $time2+=60*60*24*$days_in_year;
-								break;
+							switch ($parts[0])
+							{
+								case 'daily':
+									$time+=60*60*24;
+									if (!is_null($time2)) $time2+=60*60*24;
+									break;
+								case 'weekly':
+									$time+=60*60*24*7;
+									if (!is_null($time2)) $time2+=60*60*24*7;
+									break;
+								case 'monthly':
+									$days_in_month=intval(date('D',mktime(0,0,0,intval(date('m',$time))+1,0,intval(date('Y',$time)))));
+									$time+=60*60*$days_in_month;
+									if (!is_null($time2)) $time2+=60*60*$days_in_month;
+									break;
+								case 'yearly':
+									$days_in_year=intval(date('Y',mktime(0,0,0,0,0,intval(date('Y',$time))+1)));
+									$time+=60*60*24*$days_in_year;
+									if (!is_null($time2)) $time2+=60*60*24*$days_in_year;
+									break;
+							}
 						}
 						if ($parts[1][$i]!='0')
 						{
-							echo "DTSTART:".date('Ymd',$time)."T".date('His',$time)."\n";
-							if (!is_null($time2)) echo "DTEND:".date('Ymd',$time2).(is_null($event['e_start_hour'])?"":"T".date('His',$time2))."\n";
-							$recurrence_code='FREQ='.strtoupper($parts[0]);
-							echo "RRULE:".$recurrence_code.";INTERVAL=".strval(strlen($parts[1])).";COUNT=1\n";
+							echo "DTSTART;TZ=".$event['e_timezone'].":".date('Ymd',$time).(is_null($event['e_start_hour'])?"":("T".date('His',$time)))."\n";
+							if (!is_null($time2)) echo "DTEND:".date('Ymd',$time2)."T".(is_null($event['e_end_hour'])?"":("T".date('His',$time2)))."\n";
+							$recurrence_code='FREQ='.strtoupper($parts[0]); // MONTHLY etc
+							echo "RRULE:".$recurrence_code;
+							if (strlen($parts[1])!=1) echo ";INTERVAL=".strval(strlen($parts[1]));
+							if (!is_null($event['e_recurrences'])) echo ";COUNT=".strval($event['e_recurrences']);
+							if ($event['e_start_monthly_spec_type']!='day_of_month')
+							{
+								switch ($event['e_start_monthly_spec_type'])
+								{
+									case 'day_of_month_backwards':
+										// Not supported by iCalendar
+										break;
+									case 'dow_of_month':
+									case 'dow_of_month_backwards':
+										echo ';BYDAY=';
+										echo ($event['e_start_monthly_spec_type']=='dow_of_month')?'+':'-';
+										echo strval(intval(floatval($event['e_start_day'])/7.0+1));
+										switch ($event['e_start_day']%7)
+										{
+											case 0:
+												echo 'MO';
+												break;
+											case 1:
+												echo 'TU';
+												break;
+											case 2:
+												echo 'WE';
+												break;
+											case 3:
+												echo 'TH';
+												break;
+											case 4:
+												echo 'FR';
+												break;
+											case 5:
+												echo 'SA';
+												break;
+											case 6:
+												echo 'SU';
+												break;
+										}
+										break;
+								}
+							}
+							echo "\n";
 						}
 					}
+				} else
+				{
+					echo "DTSTART:".date('Ymd',$time)."T".date('His',$time)."\n";
+					if (!is_null($time2)) echo "DTEND:".date('Ymd',$time2).(is_null($event['e_start_hour'])?"":"T".date('His',$time2))."\n";
 				}
-			} else
-			{
-				echo "DTSTART:".date('Ymd',$time)."T".date('His',$time)."\n";
-				if (!is_null($time2)) echo "DTEND:".date('Ymd',$time2).(is_null($event['e_start_hour'])?"":"T".date('His',$time2))."\n";
-			}
 
-			$attendees=$GLOBALS['SITE_DB']->query_select('calendar_reminders',array('*'),array('e_id'=>$event['id']),'',5000/*reasonable limit*/);
-			if (count($attendees)==5000) $attendees=array();
-			foreach ($attendees as $attendee)
-			{
-				if ($attendee['n_member_id']!=get_member())
-				{
-					if (!is_guest($event['n_member_id']))
-						echo "ATTENDEE;CN=".ical_escape($GLOBALS['FORUM_DRIVER']->get_username($attendee['n_member_id'])).";DIR=".ical_escape($GLOBALS['FORUM_DRIVER']->member_profile_url($attendee['n_member_id'])).":MAILTO:".ical_escape($GLOBALS['FORUM_DRIVER']->get_member_email_address($attendee['n_member_id']))."\n";
-				} else
+				$attendees=$GLOBALS['SITE_DB']->query_select('calendar_reminders',array('*'),array('e_id'=>$event['id']),'',5000/*reasonable limit*/);
+				if (count($attendees)==5000) $attendees=array();
+				foreach ($attendees as $attendee)
 				{
-					echo "BEGIN:VALARM\n";
-					echo "X-WR-ALARMUID:alarm".ical_escape(strval($event['id']).'@'.get_base_url())."\n";
-					echo "ACTION:AUDIO\n";
-					echo "TRIGGER:-PT".strval($attendee['n_seconds_before'])."S\n";
-					echo "ATTACH;VALUE=URI:Basso\n";
-					echo "END:VALARM\n";
+					if ($attendee['n_member_id']!=get_member())
+					{
+						if (!is_guest($event['n_member_id']))
+							echo "ATTENDEE;CN=".ical_escape($GLOBALS['FORUM_DRIVER']->get_username($attendee['n_member_id'])).";DIR=".ical_escape($GLOBALS['FORUM_DRIVER']->member_profile_url($attendee['n_member_id']));
+							$addr=$GLOBALS['FORUM_DRIVER']->get_member_email_address($attendee['n_member_id']);
+							if ($addr!='') echo ":MAILTO:".ical_escape($addr);
+							echo "\n";
+					} else
+					{
+						echo "BEGIN:VALARM\n";
+						echo "X-WR-ALARMUID:alarm".ical_escape(strval($event['id']).'@'.get_base_url())."\n";
+						echo "ACTION:AUDIO\n";
+						echo "TRIGGER:-PT".strval($attendee['n_seconds_before'])."S\n";
+						echo "ATTACH;VALUE=URI:Basso\n";
+						echo "END:VALARM\n";
+					}
 				}
-			}
 
-			echo "END:VEVENT\n";
+				echo "END:VEVENT\n";
+			}
 		}
+
+		$start+=1000;
 	}
+	while (array_key_exists(0,$events));
+
 	echo "END:VCALENDAR\n";
 	exit();
 }
@@ -212,19 +278,22 @@ function ical_import($file_name)
 {
 	$data=file_get_contents($file_name);
 
-	$whole=end(explode('BEGIN:VCALENDAR',$data));
+	$exploded=explode('BEGIN:VCALENDAR',$data);
+	$whole=end($exploded);
 
 	$events=explode('BEGIN:VEVENT',$whole);
 
 	$calendar_nodes=array();
-	
+
 	$new_type=NULL;
 
-	foreach($events as $key=>$items)
+	foreach ($events as $key=>$items)
 	{		
+		$items=preg_replace('#(.*)\n +(.*)\n#','${1}${2}',$items); // Merge split lines
+
 		$nodes=explode("\n",$items);
 
-		foreach($nodes as $_child)
+		foreach ($nodes as $_child)
 		{
 			$child=explode(':',$_child,2);
 
@@ -239,7 +308,7 @@ function ical_import($file_name)
 
 		if ($key!=0)
 		{
-			list(,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)=get_event_data_ical($calendar_nodes[$key]);
+			list(,$type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes)=get_event_data_ical($calendar_nodes[$key]);
 
 			if (is_null($type))
 			{
@@ -251,7 +320,7 @@ function ical_import($file_name)
 				$type=$new_type;
 			}
 
-			$id=add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,1,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
+			$id=add_calendar_event($type,$recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,1,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
 		}
 	}
 }
@@ -294,6 +363,9 @@ function get_event_data_ical($calendar_nodes)
 	$allow_comments=1;
 	$allow_trackbacks=1;
 	$matches=array();
+	$start_monthly_spec_type='day_of_month';
+	$end_monthly_spec_type=$start_monthly_spec_type;
+	$start_monthly_spec_type_day=mixed();
 
 	$rec_array=array('FREQ','BYDAY','INTERVAL','COUNT');
 	$rec_by_day=array('MO','TU','WE','TH','FR','SA','SU');
@@ -305,12 +377,51 @@ function get_event_data_ical($calendar_nodes)
 	if (array_key_exists('RRULE',$calendar_nodes))
 	{
 		$byday='';
-		foreach($rec_array as $value)
+		foreach ($rec_array as $value)
 		{
 			if (preg_match('/^((.)*('.$value.'=))([^;]+)/i',$calendar_nodes['RRULE'],$matches)!=0)
 			{
 				switch ($value)
 				{
+					case 'BYDAY':
+						$matches2=array();
+						if (preg_match('#^([\+\-] )?(\d+) ?(MO|TU|WE|TH|FR|SA|SU)#',end($matches),$matches2)!=0)
+						{
+							if ($matches2[1]=='-')
+							{
+								$start_monthly_spec_type='dow_of_month_backwards';
+							} else
+							{
+								$start_monthly_spec_type='dow_of_month';
+							}
+							$end_monthly_spec_type=$start_monthly_spec_type;
+							switch ($matches2[3]) // The data collected here is not actually used, because it is automatically derivable
+							{
+								case 'MO':
+									$start_monthly_spec_type_day=0+(intval($matches2[2])-1)*7;
+									break;
+								case 'TU':
+									$start_monthly_spec_type_day=1+(intval($matches2[2])-1)*7;
+									break;
+								case 'WE':
+									$start_monthly_spec_type_day=2+(intval($matches2[2])-1)*7;
+									break;
+								case 'TH':
+									$start_monthly_spec_type_day=3+(intval($matches2[2])-1)*7;
+									break;
+								case 'FR':
+									$start_monthly_spec_type_day=4+(intval($matches2[2])-1)*7;
+									break;
+								case 'SA':
+									$start_monthly_spec_type_day=5+(intval($matches2[2])-1)*7;
+									break;
+								case 'SU':
+									$start_monthly_spec_type_day=6+(intval($matches2[2])-1)*7;
+									break;
+							}
+						}
+						break;
+
 					case 'FREQ':
 						$e_recurrence=strtolower(end($matches));
 						break;
@@ -318,7 +429,7 @@ function get_event_data_ical($calendar_nodes)
 					case 'INTERVAL':
 						$rec_patern=' 1';
 
-						for ($i = 1; $i < intval(end($matches)); $i++)
+						for ($i=1;$i<intval(end($matches));$i++)
 						{
 							$rec_patern.='0';
 						}
@@ -427,7 +538,17 @@ function get_event_data_ical($calendar_nodes)
 		}
 	}
 
-	$ret=array($url,$typeid,$e_recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
+	if ($start_monthly_spec_type!='day_of_month')
+	{
+		$start_day=find_abstract_day($start_year,$start_month,$start_day,$start_monthly_spec_type);
+	}
+
+	if ($end_monthly_spec_type!='day_of_month')
+	{
+		$end_day=find_abstract_day($end_year,$end_month,$end_day,$start_monthly_spec_type/*not encoded differently in iCalendar*/);
+	}
+
+	$ret=array($url,$typeid,$e_recurrence,$recurrences,$seg_recurrences,$title,$content,$priority,$is_public,$start_year,$start_month,$start_day,$start_monthly_spec_type,$start_hour,$start_minute,$end_year,$end_month,$end_day,$end_monthly_spec_type,$end_hour,$end_minute,$timezone,$validated,$allow_rating,$allow_comments,$allow_trackbacks,$notes);
 	return $ret;
 }
 
diff --git a/sources/form_templates.php b/sources/form_templates.php
index 67ab84b..0d3300f 100644
--- a/sources/form_templates.php
+++ b/sources/form_templates.php
@@ -1678,7 +1678,7 @@ function get_form_field_tabindex($tabindex=NULL)
  * @param  boolean		Whether this entry is selected by default or not
  * @param  mixed			The text associated with this choice (blank: just use name for text)
  * @param  ?integer		The tab index of the field (NULL: not specified)
- * @param  string			An additional long description (blank: no description)
+ * @param  mixed			An additional long description (blank: no description)
  * @return tempcode		The input field
  */
 function form_input_radio_entry($name,$value,$selected=false,$text='',$tabindex=NULL,$description='')
diff --git a/sources/hooks/systems/cron/calendar.php b/sources/hooks/systems/cron/calendar.php
index d65b641..423d27d 100755
--- a/sources/hooks/systems/cron/calendar.php
+++ b/sources/hooks/systems/cron/calendar.php
@@ -39,7 +39,9 @@ class Hook_cron_calendar
 			$or_list='';
 			foreach ($jobs as $job)
 			{
-				$recurrences=find_periods_recurrence($job['e_timezone'],1,$job['e_start_year'],$job['e_start_month'],$job['e_start_day'],is_null($job['e_start_hour'])?find_timezone_start_hour_in_utc($job['e_timezone'],$job['e_start_year'],$job['e_start_month'],$job['e_start_day']):$job['e_start_hour'],is_null($job['e_start_minute'])?find_timezone_start_minute_in_utc($job['e_timezone'],$job['e_start_year'],$job['e_start_month'],$job['e_start_day']):$job['e_start_minute'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day'],is_null($job['e_end_hour'])?find_timezone_end_hour_in_utc($job['e_timezone'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day']):$job['e_end_hour'],is_null($job['e_end_minute'])?find_timezone_end_minute_in_utc($job['e_timezone'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day']):$job['e_end_minute'],$job['e_recurrence'],min(1,$job['e_recurrences']));
+				$recurrences=find_periods_recurrence($job['e_timezone'],1,$job['e_start_year'],$job['e_start_month'],$job['e_start_day'],$job['e_start_monthly_spec_type'],is_null($job['e_start_hour'])?find_timezone_start_hour_in_utc($job['e_timezone'],$job['e_start_year'],$job['e_start_month'],$job['e_start_day'],$job['e_start_monthly_spec_type']):$job['e_start_hour'],is_null($job['e_start_minute'])?find_timezone_start_minute_in_utc($job['e_timezone'],$job['e_start_year'],$job['e_start_month'],$job['e_start_day'],$job['e_start_monthly_spec_type']):$job['e_start_minute'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day'],$job['e_end_monthly_spec_type'],is_null($job['e_end_hour'])?find_timezone_end_hour_in_utc($job['e_timezone'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day'],$job['e_end_monthly_spec_type']):$job['e_end_hour'],is_null($job['e_end_minute'])?find_timezone_end_minute_in_utc($job['e_timezone'],$job['e_end_year'],$job['e_end_month'],$job['e_end_day'],$job['e_end_monthly_spec_type']):$job['e_end_minute'],$job['e_recurrence'],min(1,$job['e_recurrences']));
+
+				$start_day_of_month=find_concrete_day_of_month($job['e_start_year'],$job['e_start_month'],$job['e_start_day'],$job['e_start_monthly_spec_type']);
 
 				// Dispatch
 				if (is_null($job['j_reminder_id'])) // It's code/URL
@@ -65,7 +67,8 @@ class Hook_cron_calendar
 								if ($to_echo===false) fatal_exit(@strval($php_errormsg));
 							} else
 							{
-								$GLOBALS['event_timestamp']=array_key_exists(0,$recurrences)?usertime_to_utctime($recurrences[0][0]):mktime($job['e_start_hour'],$job['e_start_minute'],0,$job['e_start_month'],$job['e_start_day'],$job['e_start_year']);
+								$GLOBALS['event_timestamp']=array_key_exists(0,$recurrences)?usertime_to_utctime($recurrences[0][0]):mktime($job['e_start_hour'],$job['e_start_minute'],0,$job['e_start_month'],$start_day_of_month,$job['e_start_year']);
+
 								// OcCLE code
 								require_code('occle');
 								$temp=new virtual_bash($job_text);
@@ -81,7 +84,7 @@ class Hook_cron_calendar
 					// Send notification
 					if (!has_category_access($job['n_member_id'],'calendar',strval($job['e_type']))) continue;
 					$title=get_translated_text($job['e_title']);
-					$timestamp=array_key_exists(0,$recurrences)?usertime_to_utctime($recurrences[0][0]):mktime($job['e_start_hour'],$job['e_start_minute'],0,$job['e_start_month'],$job['e_start_day'],$job['e_start_year']);
+					$timestamp=array_key_exists(0,$recurrences)?usertime_to_utctime($recurrences[0][0]):mktime($job['e_start_hour'],$job['e_start_minute'],0,$job['e_start_month'],$start_day_of_month,$job['e_start_year']);
 					$date=get_timezoned_date($timestamp,true,false,false,false,$job['n_member_id']);
 					$_url=build_url(array('page'=>'calendar','type'=>'view','id'=>$job['j_event_id']),get_module_zone('calendar'),NULL,false,false,true);
 					$url=$_url->evaluate();
diff --git a/sources/hooks/systems/snippets/calendar_recurrence_suggest.php b/sources/hooks/systems/snippets/calendar_recurrence_suggest.php
index e69de29..8aa9274 100644
--- a/sources/hooks/systems/snippets/calendar_recurrence_suggest.php
+++ b/sources/hooks/systems/snippets/calendar_recurrence_suggest.php
@@ -0,0 +1,43 @@
+<?php /*
+
+ ocPortal
+ Copyright (c) ocProducts, 2004-2012
+
+ See text/EN/licence.txt for full licencing information.
+
+
+ NOTE TO PROGRAMMERS:
+   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
+   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****
+
+*/
+
+/**
+ * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
+ * @copyright	ocProducts Ltd
+ * @package		calendar
+ */
+
+class Hook_calendar_recurrence_suggest
+{
+
+	/**
+	 * Standard modular run function for snippet hooks. Generates XHTML to insert into a page using AJAX.
+	 *
+	 * @return tempcode  The snippet
+	 */
+	function run()
+	{
+		require_code('calendar');
+
+		$day_of_month=get_param_integer('day');
+		$month=get_param_integer('month');
+		$year=get_param_integer('year');
+
+		$default_monthly_spec_type=get_param('monthly_spec_type');
+
+		return monthly_spec_type_chooser($day_of_month,$month,$year,$default_monthly_spec_type);
+	}
+
+}
+
