448 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			448 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/bin/bash
 | |
| 
 | |
| # Copyright (C) 2012-2013, Stachre
 | |
| #
 | |
| # This program is free software: you can redistribute it and/or modify
 | |
| # it under the terms of the GNU General Public License as published by
 | |
| # the Free Software Foundation, either version 2 of the License, or
 | |
| # (at your option) any later version.
 | |
| # 
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| # 
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>,
 | |
| # or write to the Free Software Foundation, Inc., 51 Franklin Street, 
 | |
| # Fifth Floor, Boston, MA  02110-1301, USA.
 | |
| #
 | |
| #
 | |
| # dump-contacts2db.sh
 | |
| # Version 0.4, 2013-02-11
 | |
| # Dumps contacts from an Android contacts2.db to stdout in vCard format
 | |
| # Usage:  dump-contacts2db.sh path/to/contacts2.db > path/to/output-file.vcf
 | |
| # Dependencies:  perl; base64; sqlite3 / libsqlite3-dev
 | |
| 
 | |
| function rstr {    
 | |
|     local LENGTH=${1:-8}    
 | |
|     
 | |
|     LC_ALL=C tr -dc a-z0-9 </dev/urandom | head -c $LENGTH    
 | |
| }    
 | |
| function uid {    
 | |
|     rstr 8    
 | |
|     echo -n -    
 | |
|     rstr 4    
 | |
|     echo -n -    
 | |
|     rstr 4    
 | |
|     echo -n -    
 | |
|     rstr 4    
 | |
|     echo -n -    
 | |
|     rstr 12    
 | |
| }
 | |
| 
 | |
| # From https://gist.github.com/fabacab/531019dd8a1816fc5968ebb4db3b298b
 | |
| function contact_split {
 | |
| # Bash script to separate a single vCard containing multiple contacts
 | |
| # into multiple vCard files each with its own contact.
 | |
| 
 | |
| 	dest="."
 | |
| 	if [ "$#" -ge 1 ] ; then
 | |
| 		dest="$1"	
 | |
| 	fi
 | |
| 	
 | |
| 	mkdir -p "$dest"
 | |
| 
 | |
|     local card_lines
 | |
|     local card_guid
 | |
|     declare -a card_lines
 | |
| 
 | |
| 	uid_found=false
 | |
|     while read line; do
 | |
|         if [[ "$line" =~ ^END:VCARD ]]; then
 | |
| 			if ! $uid_found ; then
 | |
| 				card_lines=("${card_lines[@]}" "UID:$(uid)")
 | |
| 			fi
 | |
| 		fi
 | |
|         # Append each line to the end of this array.
 | |
|         card_lines=("${card_lines[@]}" "$line")
 | |
| 
 | |
|         # Get the contact's GUID, this becomes the file name.
 | |
|         if [[ "$line" =~ ^FN: ]]; then
 | |
|             card_guid=$(echo -n "$line" | cut -d ':' -f 2)
 | |
|         fi
 | |
| 
 | |
| 		# Remember if we found an uid
 | |
|         if [[ "$line" =~ ^UID: ]]; then
 | |
| 			uid_found=true
 | |
|         fi
 | |
| 
 | |
|         # This is the end of the card, so output it.
 | |
|         if [[ "$line" =~ ^END:VCARD ]]; then
 | |
| 			uid_found=false
 | |
|             printf "%s\n" "${card_lines[@]}" > "$dest/$(echo -n "$card_guid" | sed 's/;/_/g' | tr 'A-Z' 'a-z' | tr -d '\r
 | |
| ').vcf"
 | |
|             unset -v card_lines
 | |
|         fi
 | |
|     done
 | |
| }
 | |
| 
 | |
| 
 | |
| # From https://github.com/stachre/dump-contacts2db
 | |
| function dump-contacts2db {
 | |
| # expects single argument, path to contacts2.db
 | |
| if [ "$#" -ne 1 ]
 | |
|     then echo -e "Dumps contacts from an Android contacts2.db to stdout in vCard format\n"
 | |
|     echo -e "Usage:  dump-contacts2db.sh path/to/contacts2.db > path/to/output-file.vcf\n"
 | |
|     echo -e "Dependencies:  perl; base64; sqlite3 / libsqlite3-dev"
 | |
|     exit 1
 | |
| fi
 | |
| 
 | |
| # TODO: verify specified contacts2.db file exists
 | |
| 
 | |
| # inits
 | |
| declare -i cur_contact_id=0
 | |
| declare -i prev_contact_id=0
 | |
| NEWLINE_QUOTED=`echo -e "'\n'"`
 | |
| MS_NEWLINE_QUOTED=`echo -e "'\r\n'"`
 | |
| CONTACTS2_PATH=$1
 | |
| 
 | |
| # store Internal Field Separator
 | |
| ORIG_IFS=$IFS
 | |
| 
 | |
| # fetch contact data
 | |
| # TODO: order by account, with delimiters if possible
 | |
| record_set=`sqlite3 $CONTACTS2_PATH "SELECT raw_contacts._id, raw_contacts.display_name, raw_contacts.display_name_alt, mimetypes.mimetype, REPLACE(REPLACE(data.data1, $MS_NEWLINE_QUOTED, '\n'), $NEWLINE_QUOTED, '\n'), data.data2, REPLACE(REPLACE(data.data4, $MS_NEWLINE_QUOTED, '\n'), $NEWLINE_QUOTED, '\n'), data.data5, data.data6, data.data7, data.data8, data.data9, data.data10, quote(data.data15) FROM raw_contacts, data, mimetypes WHERE raw_contacts.deleted = 0 AND raw_contacts._id = data.raw_contact_id AND data.mimetype_id = mimetypes._id ORDER BY raw_contacts._id, mimetypes._id, data.data2"`
 | |
| 
 | |
| # modify Internal Field Separator for parsing rows from recordset
 | |
| IFS=`echo -e "\n\r"`
 | |
| 
 | |
| # iterate through contacts data rows
 | |
| # use "for" instead of piped "while" to preserve var values post-loop
 | |
| for row in $record_set
 | |
| do
 | |
|     # modify Internal Field Separator for parsing cols from row
 | |
|     IFS="|"
 | |
| 
 | |
|     i=0
 | |
| 
 | |
|     for col in $row
 | |
|     do
 | |
|         i=$[i+1]
 | |
| 
 | |
|         # contact data fields stored in generic value columns
 | |
|         # schema determined by "mimetype", which varies by row
 | |
|         case $i in
 | |
|             1)    # raw_contacts._id
 | |
|                 cur_contact_id="$col"
 | |
|                 ;;
 | |
| 
 | |
|             2)    # raw_contacts.display_name
 | |
|                 cur_display_name=$col
 | |
|                 ;;
 | |
| 
 | |
|             3)    # raw_contacts.display_name_alt
 | |
|                 # replace comma-space with semicolon
 | |
|                 cur_display_name_alt=${col/, /\;}
 | |
|                 ;;
 | |
| 
 | |
|             4)    # mimetypes.mimetype
 | |
|                 cur_mimetype=$col
 | |
|                 ;;
 | |
| 
 | |
|             5)    # data.data1
 | |
|                 cur_data1=$col
 | |
|                 ;;
 | |
| 
 | |
|             6)    # data.data2
 | |
|                 cur_data2=$col
 | |
|                 ;;
 | |
| 
 | |
|             7)    # data.data4
 | |
|                 cur_data4=$col
 | |
|                 ;;
 | |
| 
 | |
|             8)    # data.data5
 | |
|                 cur_data5=$col
 | |
|                 ;;
 | |
| 
 | |
|             9)    # data.data6
 | |
|                 cur_data6=$col
 | |
|                 ;;
 | |
| 
 | |
|             10)    # data.data7
 | |
|                 cur_data7=$col
 | |
|                 ;;
 | |
| 
 | |
|             11)    # data.data8
 | |
|                 cur_data8=$col
 | |
|                 ;;
 | |
| 
 | |
|             12)    # data.data9
 | |
|                 cur_data9=$col
 | |
|                 ;;
 | |
| 
 | |
|             13)    # data.data10
 | |
|                 cur_data10=$col
 | |
|                 ;;
 | |
| 
 | |
|             14)    # data.data15
 | |
|                 cur_data15=$col
 | |
|                 ;;
 | |
| 
 | |
|         esac
 | |
|     done
 | |
| 
 | |
|     # new contact
 | |
|     if [ $prev_contact_id -ne $cur_contact_id ]; then
 | |
|         if [ $prev_contact_id -ne 0 ]; then
 | |
|             # echo current vcard prior to reinitializing variables
 | |
|             
 | |
|             # some contacts apps don't have IM fields; add to top of NOTE: field
 | |
|             if [ ${#cur_vcard_im_note} -ne 0 ]
 | |
|                 then cur_vcard_note=$cur_vcard_im_note"\n"$cur_vcard_note
 | |
|             fi
 | |
| 
 | |
|             # generate and echo vcard
 | |
|             if [ ${#cur_vcard_note} -ne 0 ]
 | |
|                 then cur_vcard_note="NOTE:"$cur_vcard_note$'\n'
 | |
|             fi
 | |
|             cur_vcard=$cur_vcard$cur_vcard_nick$cur_vcard_org$cur_vcard_title$cur_vcard_tel$cur_vcard_adr$cur_vcard_email$cur_vcard_url$cur_vcard_note$cur_vcard_photo$cur_vcard_im
 | |
|             cur_vcard=$cur_vcard"END:VCARD"
 | |
|             echo $cur_vcard
 | |
|         fi
 | |
| 
 | |
|         # init new vcard
 | |
|         cur_vcard="BEGIN:VCARD"$'\n'"VERSION:3.0"$'\n'
 | |
|         cur_vcard=$cur_vcard"N:"$cur_display_name_alt$'\n'"FN:"$cur_display_name$'\n'
 | |
| 		cur_vcard=$cur_vcard"UID:$(uid)"$'\n'
 | |
|         cur_vcard_nick=""
 | |
|         cur_vcard_org=""
 | |
|         cur_vcard_title=""
 | |
|         cur_vcard_tel=""
 | |
|         cur_vcard_adr=""
 | |
|         cur_vcard_email=""
 | |
|         cur_vcard_url=""
 | |
|         cur_vcard_im=""
 | |
|         cur_vcard_im_note=""
 | |
|         cur_vcard_note=""
 | |
|         cur_vcard_photo=""
 | |
|     fi
 | |
| 
 | |
|     # add current row to current vcard
 | |
|     # again, "mimetype" determines schema on a row-by-row basis
 | |
|     # TODO: handle following types
 | |
|     #   * (6) vnd.android.cursor.item/sip_address
 | |
|     #   * (7) vnd.android.cursor.item/identity (not exported by Android 4.1 Jelly Bean) 
 | |
|     #   * (13) vnd.android.cursor.item/group_membership (not exported by Android 4.1 Jelly Bean) 
 | |
|     #   * (14) vnd.com.google.cursor.item/contact_misc (not exported by Android 4.1 Jelly Bean) 
 | |
|     case $cur_mimetype in
 | |
|         vnd.android.cursor.item/nickname)
 | |
|             if [ ${#cur_data1} -ne 0 ]
 | |
|                 then cur_vcard_nick="NICKNAME:"$cur_data1$'\n'
 | |
|             fi
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/organization)
 | |
|             if [ ${#cur_data1} -ne 0 ]
 | |
|                 then cur_vcard_org=$cur_vcard_org"ORG:"$cur_data1$'\n'
 | |
|             fi
 | |
|             
 | |
|             if [ ${#cur_data4} -ne 0 ]
 | |
|                 then cur_vcard_title="TITLE:"$cur_data4$'\n'
 | |
|             fi
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/phone_v2)
 | |
|             case $cur_data2 in
 | |
|                 1)
 | |
|                     cur_vcard_tel_type="HOME,VOICE"
 | |
|                     ;;
 | |
| 
 | |
|                 2)
 | |
|                     cur_vcard_tel_type="CELL,VOICE,PREF"
 | |
|                     ;;
 | |
| 
 | |
|                 3)
 | |
|                     cur_vcard_tel_type="WORK,VOICE"
 | |
|                     ;;
 | |
| 
 | |
|                 4)
 | |
|                     cur_vcard_tel_type="WORK,FAX"
 | |
|                     ;;
 | |
| 
 | |
|                 5)
 | |
|                     cur_vcard_tel_type="HOME,FAX"
 | |
|                     ;;
 | |
| 
 | |
|                 6)
 | |
|                     cur_vcard_tel_type="PAGER"
 | |
|                     ;;
 | |
| 
 | |
|                 7)
 | |
|                     cur_vcard_tel_type="OTHER"
 | |
|                     ;;
 | |
| 
 | |
|                 8)
 | |
|                     cur_vcard_tel_type="CUSTOM"
 | |
|                     ;;
 | |
| 
 | |
|                 9)
 | |
|                     cur_vcard_tel_type="CAR,VOICE"
 | |
|                     ;;
 | |
|             esac
 | |
| 
 | |
|             cur_vcard_tel=$cur_vcard_tel"TEL;TYPE="$cur_vcard_tel_type":"$cur_data1$'\n'
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/postal-address_v2)
 | |
|             case $cur_data2 in
 | |
|                 1)
 | |
|                     cur_vcard_adr_type="HOME"
 | |
|                     ;;
 | |
| 
 | |
|                 2)
 | |
|                     cur_vcard_adr_type="WORK"
 | |
|                     ;;
 | |
|             esac
 | |
| 
 | |
|             # ignore addresses that contain only USA (MS Exchange)
 | |
|             # TODO: validate general address pattern instead
 | |
|             if [ $cur_data1 != "United States of America" ]
 | |
|                 then cur_vcard_adr=$cur_vcard_adr"ADR;TYPE="$cur_vcard_adr_type":;;"$cur_data4";"$cur_data7";"$cur_data8";"$cur_data9";"$cur_data10$'\n'
 | |
|                 cur_vcard_adr=$cur_vcard_adr"LABEL;TYPE="$cur_vcard_adr_type":"$cur_data1$'\n'
 | |
|             fi
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/email_v2)
 | |
|             cur_vcard_email=$cur_vcard_email"EMAIL:"$cur_data1$'\n'
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/website)
 | |
|             cur_vcard_url=$cur_vcard_url"URL:"$cur_data1$'\n'
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/im)
 | |
|              # handle entire string within each case to avoid unhandled cases
 | |
|              case $cur_data5 in
 | |
|                 -1)
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-Custom-"$cur_data6": "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 0)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-AIM:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-AIM: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 1)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-MSN:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-MSN: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 2)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-YAHOO:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-Yahoo: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 3)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-SKYPE-USERNAME:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-Skype: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 4)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-QQ:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-QQ: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 5)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-GOOGLE-TALK:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-Google-Talk: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 6)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-ICQ:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-ICQ: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 7)
 | |
|                     cur_vcard_im=$cur_vcard_im"X-JABBER:"$cur_data1$'\n'
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM-Jabber: "$cur_data1"\n"
 | |
|                     ;;
 | |
| 
 | |
|                 *)
 | |
|                     # Android 2.3 Gingerbread doesn't identify service; data5==""
 | |
|                     cur_vcard_im_note=$cur_vcard_im_note"IM: "$cur_data1"\n"
 | |
|                     ;;
 | |
|             esac
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/photo)
 | |
|             if [ $cur_data15 != "NULL" ]; then
 | |
|                 # Remove the prefix "X'" and suffix "'" from the sqlite3 quote(BLOB) hex output
 | |
|                 photo=`echo $cur_data15 | sed -e "s/^X'//" -e "s/'$//"`
 | |
|                 
 | |
|                 # Convert the hex to base64
 | |
|                 # TODO: optimize
 | |
|                 photo=`echo $photo | perl -ne 's/([0-9a-f]{2})/print chr hex $1/gie' | base64 --wrap=0`
 | |
|                 
 | |
|                 cur_vcard_photo=$cur_vcard_photo"PHOTO;ENCODING=BASE64;JPEG:"$photo$'\n'
 | |
|                 
 | |
|                 # TODO: line wrapping; Android import doesn't like base64's wrapping
 | |
|                 
 | |
|                 # For testing
 | |
|                 #echo $cur_data15 > "images/$cur_display_name.txt"
 | |
|                 #echo $cur_data15 | perl -ne 's/([0-9a-f]{2})/print chr hex $1/gie' > "images/$cur_display_name.jpg"
 | |
|             fi
 | |
|             ;;
 | |
| 
 | |
|         vnd.android.cursor.item/note)
 | |
|             # "NOTE:" and trailing \n appended when vCard is finished and echoed
 | |
|             if [ ${#cur_vcard_note} -ne 0 ]
 | |
|                 then cur_vcard_note=$cur_vcard_note"\n\n"$cur_data1
 | |
|                 else cur_vcard_note=$cur_data1
 | |
|             fi
 | |
|             ;;
 | |
|     esac    
 | |
| 
 | |
|     prev_contact_id=$cur_contact_id
 | |
| 
 | |
|     # reset Internal Field Separator for parent loop
 | |
|     IFS=`echo -e "\n\r"`
 | |
| done
 | |
| 
 | |
| # set Internal Field Separator to other-than-newline prior to echoing final vcard
 | |
| IFS="|"
 | |
| 
 | |
| # some contacts apps don't have IM fields; add to top of NOTE: field
 | |
| if [ ${#cur_vcard_im_note} -ne 0 ]
 | |
|     then cur_vcard_note=$cur_vcard_im_note"\n"$cur_vcard_note
 | |
| fi
 | |
| 
 | |
| # generate and echo vcard
 | |
| if [ ${#cur_vcard_note} -ne 0 ]
 | |
|     then cur_vcard_note="NOTE:"$cur_vcard_note$'\n'
 | |
| fi
 | |
| cur_vcard=$cur_vcard$cur_vcard_nick$cur_vcard_org$cur_vcard_title$cur_vcard_tel$cur_vcard_adr$cur_vcard_email$cur_vcard_url$cur_vcard_note$cur_vcard_photo$cur_vcard_im
 | |
| cur_vcard=$cur_vcard"END:VCARD"
 | |
| echo $cur_vcard
 | |
| 
 | |
| # restore original Internal Field Separator
 | |
| IFS=$ORIG_IFS
 | |
| 
 | |
| }
 | |
| 
 | |
| 
 | |
| # MAIN FUNCTION
 | |
| phone_to_vcf () {
 | |
| 	# Copy contacts from phone via adb and convert them to vcf
 | |
| 	tmp="$(mktemp -d)"
 | |
| 	
 | |
| 	echo "Get db from phone"
 | |
| 	adb pull /data/data/com.android.providers.contacts/databases/contacts2.db "$tmp"
 | |
| 	
 | |
| 	echo "Convert it to multiple vcard files"
 | |
| 	dump-contacts2db "$tmp/contacts2.db" | contact_split "${1:-$HOME/.contacts/vcards}"
 | |
| 	
 | |
| 	echo "Clean"
 | |
| 	rm -r "$tmp"
 | |
| }
 |