Printing Christmas Card Envelopes Revisited - SOLVED

Finding a straight forward way to print envelopes for christmas card mailings has not been an easy task on the mac/ios. Without mail merge support in Pages/Numbers finding a solution has been a creative endeavor. This is what I came up with…


  • iOS Contact database properly formatted
  • Pythonista on iOS
  • Install the Pythonista Shell (staSH)
  • Install FPDF module

Contacts Setup


There will be one main contact card per family to generate the printed envelope. This would generally be the person you want addressed first. Make sure the first name and last name are inputed properly in the contact card. If you want spouses, partners, or children also included, then those names have to be added to the “add relation” section of the contact card.

Any card that you want included needs to have the hashtag #xmas in the note of the card. As long as you keep this up to date then mailings will be easy.

The output would be like this:

  • Single: Johnny Appleseed
  • Married same last name: Johnny and Mary Appleseed
  • Married/Partner/Sig Other different last name: Johnny Appleseed and Mary Smith
  • Kids would be added on the line below: and Kid1, Kid2, Kid3


Contacts can have multiple addresses. So the script looks for a “home” address. If there is a “home” label and a “mail” label then the “mail” label is used. No address will be used if it can’t find one of these labels.


The Pythonista SHell needs to be installed so that the FDPF module can be installed into the app.

Go here for installation instructions


This is used generate one page of a PDF for each envelope. You can customize the size of each page, the margins, and add graphics.

Using the staSH:
pip install fpdf

That should make it available to your script.
Documentation here


For you to use/review

import contacts
from fpdf import FPDF

# CONSTANTS________

# Ask user for the height and width of envelope
ENVELOPE_H = float(input('Height(inches): '))
ENVELOPE_W = float(input('Width(inches): '))

# This is used for the height of the text cells

# Adjust these for where you want the addressee text placed on the page. The defaults are 40% from the left and 45% from the top 


def getMailAddress(address_dict):
	''' Gets the address dictionary from a contact and returns a tuple of information.
	(List of streets,city string,state string,zip string,country string)
	street = address_dict['Street'].strip().split('\n')
	city = address_dict['City']
	state = address_dict['State']
	zip = address_dict['ZIP']
	country = ''
	if address_dict[contacts.COUNTRY] != 'United States':
		country =  address_dict[contacts.COUNTRY].upper()
	the_address = (street,city,state,zip,country)
	return the_address

def add_return_address():
	''' Adds the return address to the top left of the document.
	# Change to your info
	pdf.cell(w=0,h=HEIGHT,txt='YOUR NAME',border=0,ln=1)
	pdf.cell(w=0,h=HEIGHT,txt='123 Example Street',border=0,ln=1)
	pdf.cell(w=0,h=HEIGHT,txt='city, State. ZIP',border=0,ln=1)
	#TRY USING AN IMAGE: uncomment the next line and replace the NAME for your image
	# The numbers represent the x,y position and the width you want it to be, height auto calulates

def add_mailing_block(x,h,text):
	''' Adds a line to the addressee block.

# CREATE THE PDF__________

# Gather all contacts
all_ppl = contacts.get_all_people()

# Loop through each person
for person in all_ppl:
	# Reset defaults
	has_children = False
	contact_output = ''
	children = ''
	mail_to = ''
	# Only deal with 'xmas' tagged people
	if '#xmas' in person.note:
		# Add a new PDF page for this contact
		# Collect all addresses in the contact which is a list of tupels in the form [(Label String,Address Dictionary)]
		addresses = person.address
		if addresses:
			for address in addresses:
				if address[0] == 'mail':
					mail_to = getMailAddress(address[1])
				elif address[0] == '_$!<Home>!$_':
					mail_to = getMailAddress(address[1])
				mail_to = (['--No valid address--'],'','','','')
		# Look for related names
		# ie. have spouses, partner, kids
		if person.related_names:
			# Gather all children
			children_list = [x[1] for x in person.related_names if x[0] == '_$!<Child>!$_']
			# Gather all spouses
			spouse_list = [x[1] for x in person.related_names if x[0] == '_$!<Spouse>!$_']
			# Gather all girlfriend/boyfriend or significant others with a different last name which are labeled 'partner'
			partner_list = [x[1] for x in person.related_names if x[0] == '_$!<Partner>!$_']
			for relation_tuple in person.related_names:
				# Make a child list if exists
				if children_list:
					has_children = True
					children = ', '.join(children_list)
				# Married
				if spouse_list:
					contact_output += person.first_name + ' & ' + ''.join(spouse_list) + ' ' + person.last_name
				# Has a significant other or different last name
				elif partner_list:
					contact_output += person.full_name + ' and ' + ''.join(partner_list)
				# Single with kids
					contact_output += person.full_name
		# Single
			contact_output += person.full_name
		# Move the cursor to the mailing block
		# Add a line for the kids if necessary
		if has_children:
			add_mailing_block(SENDER_POSITION_X,HEIGHT,'and '+children)

		# Start adding the address lines
		for line in mail_to[0]:
		# Construct the city state zip string and add to the mail block
		city_state_zip = f'{mail_to[1]}, {mail_to[2]}  {mail_to[3]}'
		# Add a line for a country other than the US
		if mail_to[4]:

# Save the file

Sample Output

I’ll can try to generate some sample data to show what the output can look like.

1 Like