├── Crypt ├── SSLeay.pm └── SSLeay │ ├── CTX.pm │ ├── Conn.pm │ ├── Err.pm │ ├── MainContext.pm │ └── X509.pm ├── Debug.pl ├── README.md ├── SimplenoteSync.pl └── nuclear-option.pl /Crypt/SSLeay.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay; 2 | 3 | use strict; 4 | use vars '$VERSION'; 5 | $VERSION = '0.57'; 6 | 7 | eval { 8 | require XSLoader; 9 | XSLoader::load('Crypt::SSLeay', $VERSION); 10 | 1; 11 | } 12 | or do { 13 | require DynaLoader; 14 | use vars '@ISA'; # not really locally scoped, it just looks that way 15 | @ISA = qw(DynaLoader); 16 | bootstrap Crypt::SSLeay $VERSION; 17 | }; 18 | 19 | use vars qw(%CIPHERS); 20 | %CIPHERS = ( 21 | 'NULL-MD5' => "No encryption with a MD5 MAC", 22 | 'RC4-MD5' => "128 bit RC4 encryption with a MD5 MAC", 23 | 'EXP-RC4-MD5' => "40 bit RC4 encryption with a MD5 MAC", 24 | 'RC2-CBC-MD5' => "128 bit RC2 encryption with a MD5 MAC", 25 | 'EXP-RC2-CBC-MD5' => "40 bit RC2 encryption with a MD5 MAC", 26 | 'IDEA-CBC-MD5' => "128 bit IDEA encryption with a MD5 MAC", 27 | 'DES-CBC-MD5' => "56 bit DES encryption with a MD5 MAC", 28 | 'DES-CBC-SHA' => "56 bit DES encryption with a SHA MAC", 29 | 'DES-CBC3-MD5' => "192 bit EDE3 DES encryption with a MD5 MAC", 30 | 'DES-CBC3-SHA' => "192 bit EDE3 DES encryption with a SHA MAC", 31 | 'DES-CFB-M1' => "56 bit CFB64 DES encryption with a one byte MD5 MAC", 32 | ); 33 | 34 | use Crypt::SSLeay::X509; 35 | 36 | # A xsupp bug made this nessesary 37 | sub Crypt::SSLeay::CTX::DESTROY { shift->free; } 38 | sub Crypt::SSLeay::Conn::DESTROY { shift->free; } 39 | sub Crypt::SSLeay::X509::DESTROY { shift->free; } 40 | 41 | 1; 42 | 43 | __END__ 44 | 45 | =head1 NAME 46 | 47 | Crypt::SSLeay - OpenSSL support for LWP 48 | 49 | =head1 SYNOPSIS 50 | 51 | lwp-request https://www.example.com 52 | 53 | use LWP::UserAgent; 54 | my $ua = LWP::UserAgent->new; 55 | my $req = HTTP::Request->new('GET', 'https://www.example.com/'); 56 | my $res = $ua->request($req); 57 | print $res->content, "\n"; 58 | 59 | =head1 DESCRIPTION 60 | 61 | This document describes C version 0.57, released 62 | 2007-09-17. 63 | 64 | This perl module provides support for the https protocol under LWP, 65 | to allow an C object to perform GET, HEAD and POST 66 | requests. Please see LWP for more information on POST requests. 67 | 68 | The C package provides C, which is loaded 69 | by C for https requests and provides the 70 | necessary SSL glue. 71 | 72 | This distribution also makes following deprecated modules available: 73 | 74 | Crypt::SSLeay::CTX 75 | Crypt::SSLeay::Conn 76 | Crypt::SSLeay::X509 77 | 78 | Work on Crypt::SSLeay has been continued only to provide https 79 | support for the LWP (libwww-perl) libraries. 80 | 81 | =head1 ENVIRONMENT VARIABLES 82 | 83 | The following environment variables change the way 84 | C and C behave. 85 | 86 | # proxy support 87 | $ENV{HTTPS_PROXY} = 'http://proxy_hostname_or_ip:port'; 88 | 89 | # proxy_basic_auth 90 | $ENV{HTTPS_PROXY_USERNAME} = 'username'; 91 | $ENV{HTTPS_PROXY_PASSWORD} = 'password'; 92 | 93 | # debugging (SSL diagnostics) 94 | $ENV{HTTPS_DEBUG} = 1; 95 | 96 | # default ssl version 97 | $ENV{HTTPS_VERSION} = '3'; 98 | 99 | # client certificate support 100 | $ENV{HTTPS_CERT_FILE} = 'certs/notacacert.pem'; 101 | $ENV{HTTPS_KEY_FILE} = 'certs/notacakeynopass.pem'; 102 | 103 | # CA cert peer verification 104 | $ENV{HTTPS_CA_FILE} = 'certs/ca-bundle.crt'; 105 | $ENV{HTTPS_CA_DIR} = 'certs/'; 106 | 107 | # Client PKCS12 cert support 108 | $ENV{HTTPS_PKCS12_FILE} = 'certs/pkcs12.pkcs12'; 109 | $ENV{HTTPS_PKCS12_PASSWORD} = 'PKCS12_PASSWORD'; 110 | 111 | =head1 INSTALL 112 | 113 | =head2 OpenSSL 114 | 115 | You must have OpenSSL or SSLeay installed before compiling 116 | this module. You can get the latest OpenSSL package from: 117 | 118 | http://www.openssl.org/ 119 | 120 | On Debian systems, you will need to install the libssl-dev package, 121 | at least for the duration of the build (it may be removed afterwards). 122 | 123 | Other package-based systems may require something similar. The key 124 | is that Crypt::SSLeay makes calls to the OpenSSL library, and how 125 | to do so is specified in the C header files that come with the 126 | library. Some systems break out the header files into a separate 127 | package from that of the libraries. Once the program has been built, 128 | you don't need the headers any more. 129 | 130 | When installing openssl make sure your config looks like: 131 | 132 | ./config --openssldir=/usr/local/openssl 133 | or 134 | ./config --openssldir=/usr/local/ssl 135 | 136 | If you are planning on upgrading the default OpenSSL libraries on 137 | a system like RedHat, (not recommended), then try something like: 138 | 139 | ./config --openssldir=/usr --shared 140 | 141 | The --shared option to config will set up building the .so 142 | shared libraries which is important for such systems. This is 143 | followed by: 144 | 145 | make 146 | make test 147 | make install 148 | 149 | This way Crypt::SSLeay will pick up the includes and 150 | libraries automatically. If your includes end up 151 | going into a separate directory like /usr/local/include, 152 | then you may need to symlink /usr/local/openssl/include 153 | to /usr/local/include 154 | 155 | =head2 Crypt::SSLeay 156 | 157 | The latest Crypt::SSLeay can be found at your nearest CPAN, 158 | as well as: 159 | 160 | http://search.cpan.org/dist/Crypt-SSLeay/ 161 | 162 | Once you have downloaded it, Crypt::SSLeay installs easily 163 | using the C * commands as shown below. 164 | 165 | perl Makefile.PL 166 | make 167 | make test 168 | make install 169 | 170 | * use nmake or dmake on Win32 171 | 172 | For unattended (batch) installations, to be absolutely certain that 173 | F does not prompt for questions on STDIN, set the 174 | following environment variable beforehand: 175 | 176 | PERL_MM_USE_DEFAULT=1 177 | 178 | (This is true for any CPAN module that uses C). 179 | 180 | =head3 Windows 181 | 182 | C builds correctly with Strawberry Perl. 183 | 184 | For Activestate users, the ActiveState company does not have a 185 | permit from the Canadian Federal Government to distribute cryptographic 186 | software. This prevents C from being distributed as 187 | a PPM package from their repository. See 188 | L 189 | for more information on this issue. 190 | 191 | You may download it from Randy Kobes's PPM repository by using 192 | the following command: 193 | 194 | ppm install http://theoryx5.uwinnipeg.ca/ppms/Crypt-SSLeay.ppd 195 | 196 | An alternative is to add the uwinnipeg.ca PPM repository to your 197 | local installation. See L 198 | for more details. 199 | 200 | =head3 VMS 201 | 202 | It is assumed that the OpenSSL installation is located at 203 | C. Define this logical to point to the appropriate 204 | place in the filesystem. 205 | 206 | =head1 PROXY SUPPORT 207 | 208 | LWP::UserAgent and Crypt::SSLeay have their own versions of 209 | proxy support. Please read these sections to see which one 210 | is appropriate. 211 | 212 | =head2 LWP::UserAgent proxy support 213 | 214 | LWP::UserAgent has its own methods of proxying which may work for 215 | you and is likely to be incompatible with Crypt::SSLeay proxy support. 216 | To use LWP::UserAgent proxy support, try something like: 217 | 218 | my $ua = new LWP::UserAgent; 219 | $ua->proxy([qw( https http )], "$proxy_ip:$proxy_port"); 220 | 221 | At the time of this writing, libwww v5.6 seems to proxy https 222 | requests fine with an Apache mod_proxy server. It sends a line like: 223 | 224 | GET https://www.example.com HTTP/1.1 225 | 226 | to the proxy server, which is not the CONNECT request that 227 | some proxies would expect, so this may not work with other 228 | proxy servers than mod_proxy. The CONNECT method is used 229 | by Crypt::SSLeay's internal proxy support. 230 | 231 | =head2 Crypt::SSLeay proxy support 232 | 233 | For native Crypt::SSLeay proxy support of https requests, 234 | you need to set the environment variable C to your 235 | proxy server and port, as in: 236 | 237 | # proxy support 238 | $ENV{HTTPS_PROXY} = 'http://proxy_hostname_or_ip:port'; 239 | $ENV{HTTPS_PROXY} = '127.0.0.1:8080'; 240 | 241 | Use of the C environment variable in this way 242 | is similar to Cenv_proxy()> usage, but calling 243 | that method will likely override or break the Crypt::SSLeay 244 | support, so do not mix the two. 245 | 246 | Basic auth credentials to the proxy server can be provided 247 | this way: 248 | 249 | # proxy_basic_auth 250 | $ENV{HTTPS_PROXY_USERNAME} = 'username'; 251 | $ENV{HTTPS_PROXY_PASSWORD} = 'password'; 252 | 253 | For an example of LWP scripting with C native proxy 254 | support, please look at the F script in the 255 | C distribution. 256 | 257 | =head1 CLIENT CERTIFICATE SUPPORT 258 | 259 | Client certificates are supported. PEM0encoded certificate and 260 | private key files may be used like this: 261 | 262 | $ENV{HTTPS_CERT_FILE} = 'certs/notacacert.pem'; 263 | $ENV{HTTPS_KEY_FILE} = 'certs/notacakeynopass.pem'; 264 | 265 | You may test your files with the F program, 266 | bundled with the distribution, by issuing a command like: 267 | 268 | perl eg/net-ssl-test -cert=certs/notacacert.pem \ 269 | -key=certs/notacakeynopass.pem -d GET $HOST_NAME 270 | 271 | Additionally, if you would like to tell the client where 272 | the CA file is, you may set these. 273 | 274 | $ENV{HTTPS_CA_FILE} = "some_file"; 275 | $ENV{HTTPS_CA_DIR} = "some_dir"; 276 | 277 | There is no sample CA cert file at this time for testing, 278 | but you may configure F to use your CA cert 279 | with the -CAfile option. (TODO: then what is the ./certs 280 | directory in the distribution?) 281 | 282 | =head2 Creating a test certificate 283 | 284 | To create simple test certificates with OpenSSL, you may 285 | run the following command: 286 | 287 | openssl req -config /usr/local/openssl/openssl.cnf \ 288 | -new -days 365 -newkey rsa:1024 -x509 \ 289 | -keyout notacakey.pem -out notacacert.pem 290 | 291 | To remove the pass phrase from the key file, run: 292 | 293 | openssl rsa -in notacakey.pem -out notacakeynopass.pem 294 | 295 | =head2 PKCS12 support 296 | 297 | The directives for enabling use of PKCS12 certificates is: 298 | 299 | $ENV{HTTPS_PKCS12_FILE} = 'certs/pkcs12.pkcs12'; 300 | $ENV{HTTPS_PKCS12_PASSWORD} = 'PKCS12_PASSWORD'; 301 | 302 | Use of this type of certificate takes precedence over previous 303 | certificate settings described. (TODO: unclear? Meaning "the 304 | presence of this type of certificate??) 305 | 306 | =head1 SSL versions 307 | 308 | Crypt::SSLeay tries very hard to connect to I SSL web server 309 | accomodating servers that are buggy, old or simply 310 | not standards-compliant. To this effect, this module will 311 | try SSL connections in this order: 312 | 313 | SSL v23 - should allow v2 and v3 servers to pick their best type 314 | SSL v3 - best connection type 315 | SSL v2 - old connection type 316 | 317 | Unfortunately, some servers seem not to handle a reconnect 318 | to SSL v3 after a failed connect of SSL v23 is tried, 319 | so you may set before using LWP or Net::SSL: 320 | 321 | $ENV{HTTPS_VERSION} = 3; 322 | 323 | to force a version 3 SSL connection first. At this time only a 324 | version 2 SSL connection will be tried after this, as the connection 325 | attempt order remains unchanged by this setting. 326 | 327 | =head1 ACKNOWLEDGEMENTS 328 | 329 | Many thanks to Gisle Aas for writing this module and many others 330 | including libwww, for perl. The web will never be the same :) 331 | 332 | Ben Laurie deserves kudos for his excellent patches for better error 333 | handling, SSL information inspection, and random seeding. 334 | 335 | Thanks to Dongqiang Bai for host name resolution fix when using a 336 | proxy. 337 | 338 | Thanks to Stuart Horner of Core Communications, Inc. who found the 339 | need for building --shared OpenSSL libraries. 340 | 341 | Thanks to Pavel Hlavnicka for a patch for freeing memory when using 342 | a pkcs12 file, and for inspiring more robust read() behavior. 343 | 344 | James Woodyatt is a champ for finding a ridiculous memory leak that 345 | has been the bane of many a Crypt::SSLeay user. 346 | 347 | Thanks to Bryan Hart for his patch adding proxy support, 348 | and thanks to Tobias Manthey for submitting another approach. 349 | 350 | Thanks to Alex Rhomberg for Alpha linux ccc patch. 351 | 352 | Thanks to Tobias Manthey for his patches for client certificate 353 | support. 354 | 355 | Thanks to Daisuke Kuroda for adding PKCS12 certificate support. 356 | 357 | Thanks to Gamid Isayev for CA cert support and insights into error 358 | messaging. 359 | 360 | Thanks to Jeff Long for working through a tricky CA cert SSLClientVerify 361 | issue. 362 | 363 | Thanks to Chip Turner for patch to build under perl 5.8.0. 364 | 365 | Thanks to Joshua Chamas for the time he spent maintaining the 366 | module. 367 | 368 | Thanks to Jeff Lavallee for help with alarms on read failures (CPAN 369 | bug #12444). 370 | 371 | Thanks to Guenter Knauf for significant improvements in configuring 372 | things in Win32 and Netware lands and Jan Dubois for various 373 | suggestions for improvements. 374 | 375 | =head1 SEE ALSO 376 | 377 | =over 4 378 | 379 | =item Net::SSL 380 | 381 | If you have downloaded this distribution as of a dependency 382 | of another distribution, it's probably due to this module 383 | (which is included in this distribution). 384 | 385 | =item Net::SSLeay 386 | 387 | A module that offers access to the OpenSSL API directly from Perl. 388 | 389 | http://search.cpan.org/dist/Net_SSLeay.pm/ 390 | 391 | =item http://www.openssl.org/related/binaries.html 392 | 393 | Pointers on where to find OpenSSL binary packages (Windows). 394 | 395 | =back 396 | 397 | =head1 SUPPORT 398 | 399 | For use of Crypt::SSLeay & Net::SSL with perl's LWP, please 400 | send email to C. 401 | 402 | For OpenSSL or general SSL support please email the 403 | openssl user mailing list at C. 404 | This includes issues associated with building and installing 405 | OpenSSL on one's system. 406 | 407 | Please report all bugs at 408 | L. 409 | 410 | This module was originally written by Gisle Aas, and was subsequently 411 | maintained by Joshua Chamas. It is currently maintained by David 412 | Landgren. 413 | 414 | =head1 COPYRIGHT 415 | 416 | Copyright (c) 2006-2007 David Landgren. 417 | Copyright (c) 1999-2003 Joshua Chamas. 418 | Copyright (c) 1998 Gisle Aas. 419 | 420 | This program is free software; you can redistribute 421 | it and/or modify it under the same terms as Perl itself. 422 | 423 | =cut 424 | -------------------------------------------------------------------------------- /Crypt/SSLeay/CTX.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay::CTX; 2 | require Crypt::SSLeay; 3 | use strict; 4 | 1; 5 | -------------------------------------------------------------------------------- /Crypt/SSLeay/Conn.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay::Conn; 2 | require Crypt::SSLeay; 3 | use strict; 4 | 1; 5 | -------------------------------------------------------------------------------- /Crypt/SSLeay/Err.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay::Err; 2 | require Crypt::SSLeay; 3 | use strict; 4 | 1; 5 | -------------------------------------------------------------------------------- /Crypt/SSLeay/MainContext.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay::MainContext; 2 | 3 | # maintains a single instance of the Crypt::SSLeay::CTX class 4 | 5 | use strict; 6 | use Carp (); 7 | 8 | require Crypt::SSLeay::CTX; 9 | 10 | my $ctx = &main_ctx(); 11 | 12 | sub main_ctx { 13 | my $ssl_version = shift || 23; 14 | 15 | my $ctx = Crypt::SSLeay::CTX->new($ssl_version); 16 | $ctx->set_cipher_list($ENV{CRYPT_SSLEAY_CIPHER}) 17 | if $ENV{CRYPT_SSLEAY_CIPHER}; 18 | 19 | $ctx; 20 | } 21 | 22 | my %sub_cache = ('main_ctx' => \&main_ctx ); 23 | 24 | sub import { 25 | my $pkg = shift; 26 | my $callpkg = caller(); 27 | my @func = @_; 28 | for (@func) { 29 | s/^&//; 30 | Carp::croak("Can't export $_ from $pkg") if /\W/;; 31 | my $sub = $sub_cache{$_}; 32 | unless ($sub) { 33 | my $method = $_; 34 | $method =~ s/^main_ctx_//; # optional prefix 35 | $sub = $sub_cache{$_} = sub { $ctx->$method(@_) }; 36 | } 37 | no strict 'refs'; 38 | *{"${callpkg}::$_"} = $sub; 39 | } 40 | } 41 | 42 | 1; 43 | -------------------------------------------------------------------------------- /Crypt/SSLeay/X509.pm: -------------------------------------------------------------------------------- 1 | package Crypt::SSLeay::X509; 2 | 3 | use strict; 4 | 5 | sub not_before { 6 | my $cert = shift; 7 | not_string2time($cert->get_notBeforeString); 8 | } 9 | 10 | sub not_after { 11 | my $cert = shift; 12 | not_string2time($cert->get_notAfterString); 13 | } 14 | 15 | sub not_string2time { 16 | my $string = shift; 17 | # $string has the form 021019235959Z 18 | my($year, $month, $day, $hour, $minute, $second, $GMT)= 19 | $string=~m/(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(Z)?/; 20 | $year += 2000; 21 | my $time="$year-$month-$day $hour:$minute:$second"; 22 | $time .= " GMT" if $GMT; 23 | $time; 24 | } 25 | 26 | 1; 27 | -------------------------------------------------------------------------------- /Debug.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # 3 | # Debug.pl 4 | # 5 | # Copyright (c) 2009 Fletcher T. Penney 6 | # 7 | # 8 | # 9 | 10 | # This routine is designed to create a log file that I can use to try and help 11 | # debug when SimplenoteSync isn't working. 12 | # If we hit a point where we need it, I'll ask you to send me a copy of the 13 | # log file from your home directory 14 | # 15 | # Of note, it will log the titles of files on your computer, but will not log 16 | # your email address or password. 17 | # 18 | # 19 | # So, do the following: 20 | # 21 | # First - make sure that you synchronize your iPhone app so it and web app 22 | # are up to date 23 | # 24 | # Make sure there is at least one note on the iPhone that is also on the web 25 | # 26 | # Synchronize iphone again if necessary 27 | # 28 | # Make sure there is at least one text file in your local directory - 29 | # remember that this file must have ".txt" as the file extension 30 | # 31 | # Make sure that you have the latest versions of this software from my web 32 | # site 33 | # 34 | # Enable debug mode in SimplenoteSync.pl 35 | # 36 | # Run "SimplenoteSync.pl > ~/SimplenoteSyncDebug.txt" 37 | # 38 | # Run "Debug.pl" 39 | # 40 | # Open SimplenoteSyncDebug.txt and SimplenoteSyncLog.txt and make sure no 41 | # confidential information is included 42 | # 43 | # Email me copies of those two files along with a description of the problem 44 | # that you're having 45 | # 46 | 47 | use strict; 48 | use warnings; 49 | use File::Basename; 50 | use File::Path; 51 | use Cwd; 52 | use Cwd 'abs_path'; 53 | use MIME::Base64; 54 | use LWP::UserAgent; 55 | my $ua = LWP::UserAgent->new; 56 | use Time::Local; 57 | use File::Copy; 58 | 59 | 60 | # Configuration 61 | # 62 | # Create file in your home directory named ".simplenotesyncrc" 63 | # First line is your email address 64 | # Second line is your Simplenote password 65 | # Third line is the directory to be used for text files 66 | 67 | open (CONFIG, "<$ENV{HOME}/.simplenotesyncrc") or die "Unable to load config file $ENV{HOME}/.simplenotesyncrc.\n"; 68 | 69 | my $email = ; 70 | my $password = ; 71 | my $rc_directory = ; 72 | my $sync_directory; 73 | 74 | close CONFIG; 75 | chomp ($email, $password, $rc_directory); 76 | 77 | if ($rc_directory eq "") { 78 | # If a valid directory isn't specified, then don't keep going 79 | die "A directory was not specified.\n"; 80 | }; 81 | $rc_directory =~ s/\\ / /g; 82 | if ($sync_directory = abs_path($rc_directory)) { 83 | } else { 84 | # If a valid directory isn't specified, then don't keep going 85 | die "$rc_directory does not appear to be a valid directory.\n"; 86 | }; 87 | $sync_directory =~ s/ /\\ /g; 88 | 89 | open (LOG, ">$ENV{HOME}/SimplenoteSyncLog.txt"); 90 | print LOG ".simplenotesyncrc:\n"; 91 | my $temp = $email; 92 | $temp =~ s/[A-Za-z]/\#/g; 93 | print LOG $temp . "\npassword redacted\n"; 94 | print LOG "$sync_directory\n\n"; 95 | 96 | my $url = 'https://simple-note.appspot.com/api/'; 97 | 98 | # Document Mac OS X version 99 | my $mac_version = readpipe ("sw_vers"); 100 | 101 | print LOG "Mac OS X:\n$mac_version\n\n"; 102 | 103 | 104 | # Document token (proof of successful login to Simplenote) 105 | my $token = getToken(); 106 | print LOG "Token:\n$token\n\n"; 107 | 108 | # Document contents of simplenote account (keys only - no private info) 109 | getNoteIndex(); 110 | 111 | # Document list of ".txt" files in your specified directory, along with 112 | # mod/create times 113 | print LOG "Looking for .txt files in \"$sync_directory\"\n"; 114 | checkLocalDirectory(); 115 | 116 | # Do a list of all files in the directory, in case user is forgetting about 117 | # .txt extension 118 | my $filelist = readpipe("ls $sync_directory"); 119 | print LOG "Complete file listing:\n$filelist\n\n"; 120 | 121 | # append the contents of simplenotesync.db 122 | print LOG "simplenotesync.db:\n"; 123 | close LOG; 124 | system ("cat $sync_directory/simplenotesync.db >> $ENV{HOME}/SimplenoteSyncLog.txt"); 125 | 126 | 1; 127 | 128 | sub getToken { 129 | # Connect to server and get a authentication token 130 | 131 | my $content = encode_base64("email=$email&password=$password"); 132 | 133 | my $response = $ua->post($url . "login", Content => $content); 134 | 135 | if ($response->content =~ /Invalid argument/) { 136 | die "Problem connecting to web server.\nHave you installed Crypt:SSLeay as instructed?\n"; 137 | } 138 | 139 | 140 | die "Error logging into Simplenote server:\n$response->content\n" unless $response->is_success; 141 | 142 | 143 | return $response->content; 144 | } 145 | 146 | 147 | sub getNoteIndex { 148 | # Get list of notes from simplenote server 149 | my %note = (); 150 | 151 | my $response = $ua->get($url . "index?auth=$token&email=$email"); 152 | my $index = $response->content; 153 | 154 | print LOG "Index from Simplenote:\n$index\n\n"; 155 | } 156 | 157 | 158 | sub checkLocalDirectory { 159 | print LOG "Local Files:\n"; 160 | foreach my $filepath (glob("$sync_directory/*.txt")) { 161 | 162 | print LOG "$filepath:\n"; 163 | my @d=gmtime ((stat("$filepath"))[9]); 164 | printf LOG "\tmodify: %4d-%02d-%02d %02d:%02d:%02d\n", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 165 | 166 | @d = gmtime (readpipe ("stat -f \"%B\" \"$filepath\"")); 167 | printf LOG "\tcreate: %4d-%02d-%02d %02d:%02d:%02d\n\n", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 168 | } 169 | 170 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Name # 3 | 4 | SimplenoteSync.pl --- synchronize a folder of text files with 5 | Simplenote. 6 | 7 | Of note, this software is not created by or endorsed by Cloud Factory, 8 | the creators of Simplenote, or anyone else for that matter. 9 | 10 | 11 | # Configuration # 12 | 13 | **UPDATE** --- Notational Velocity now has built in synchronizing with 14 | Simplenote. I have not fully tested it, and can't vouch for or against it's 15 | quality. But, for anyone who is using Simplenotesync just so that their NV 16 | notes and Simplenotes stay in sync, it is probably a *much* easier way to 17 | accomplish this. Most of the support questions I get are from people who are 18 | not very experienced with the command-line --- Notational Velocity's built in 19 | support requires nothing more than your Simplenote user name and password. 20 | Additionally, I am now primarily using WriteRoom, and am not actively working 21 | on SimplenoteSync anymore. For more information, please visit: 22 | 23 | 24 | 25 | **WARNING --- I am having an intermittent problem with the Simplenote server 26 | that causes files to be deleted intermittently. Please use with caution and 27 | backup your data** 28 | 29 | **BACKUP YOUR DATA BEFORE USING --- THIS PROJECT IS STILL BEING TESTED. 30 | IF YOU AREN'T CONFIDENT IN WHAT YOU'RE DOING, DON'T USE IT!!!!** 31 | 32 | Create file in your home directory named ".simplenotesyncrc" with the 33 | following contents: 34 | 35 | 1. First line is your email address 36 | 37 | 2. Second line is your Simplenote password 38 | 39 | 3. Third line is the directory to be used for text files 40 | 41 | 4. Fourth (optional line) is a file extension to use (defaults to "txt" 42 | if none specified) 43 | 44 | Unfortunately, you have to install Crypt::SSLeay to get https to work. 45 | You can do this by running the following command as an administrator: 46 | 47 | sudo perl -MCPAN -e "install Crypt::SSLeay" 48 | 49 | 50 | # Description # 51 | 52 | After specifying a folder to store local text files, and the email 53 | address and password associated with your Simplenote account, 54 | SimplenoteSync will attempt to synchronize the information in both 55 | places. 56 | 57 | Sync information is stored in "simplenotesync.db". If this file is lost, 58 | SimplenoteSync will have to attempt to look for "collisions" between 59 | local files and existing notes. When performing the first 60 | synchronization, it's best to start with an empty local folder (or an 61 | empty collection of notes on Simplenote), and then start adding files 62 | (or notes) afterwards. 63 | 64 | 65 | # Warning # 66 | 67 | Please note that this software is still in development stages --- I 68 | STRONGLY urge you to backup all of your data before running to ensure 69 | nothing is lost. If you run SimplenoteSync on an empty local folder 70 | without a "simplenotesync.db" file, the net result will be to copy the 71 | remote notes to the local folder, effectively performing a backup. 72 | 73 | 74 | # Installation # 75 | 76 | Download the latest copy of SimplenoteSync.pl from github: 77 | 78 | 79 | 80 | 81 | # Features # 82 | 83 | * Bidirectional synchronization between the Simplenote web site and a 84 | local directory of text files on your computer 85 | 86 | * Ability to upload notes to your iPhone without typing them by hand 87 | 88 | * Ability to backup the notes on your iPhone 89 | 90 | * Perform synchronizations automatically by using cron 91 | 92 | * Should handle unicode characters in title and content (works for me in 93 | some basic tests, but let me know if you have trouble) 94 | 95 | * The ability to manipulate your notes (via the local text files) using 96 | other applications (e.g. [Notational Velocity](http://notational.net/) 97 | if you use "Plain Text Files" for storage, shell scripts, AppleScript, 98 | [TaskPaper](http://www.hogbaysoftware.com/products/taskpaper), etc.) - 99 | you're limited only by your imagination 100 | 101 | * COMING SOON --- The ability to attempt to merge changes if a note is 102 | changed locally and on the server simultaneously 103 | 104 | 105 | # Limitations # 106 | 107 | * Certain characters are prohibited in filenames (:,\,/) - if present in 108 | the title, they are stripped out. 109 | 110 | * If the simplenotesync.db file is lost, SimplenoteSync.pl is currently 111 | unable to realize that a text file and a note represent the same object 112 | --- instead you should move your local text files, do a fresh sync to 113 | download all notes locally, and manually replace any missing notes. 114 | 115 | * Simplenote supports multiple notes with the same title, but two files 116 | cannot share the same filename. If you have two notes with the same 117 | title, only one will be downloaded. I suggest changing the title of the 118 | other note. 119 | 120 | 121 | # Faq # 122 | 123 | * When I try to use SimplenoteSync, I get the following error: 124 | 125 | Network: get token 126 | Error logging into Simplenote server: 127 | HTTP::Response=HASH(0x1009b0110)->content 128 | 129 | The only time I have seen this error is when the username or 130 | password is entered into the configuration file incorrectly. Watch 131 | out for spaces at the end of lines. 132 | 133 | * Why can I download notes from Simplenote, but local notes aren't being 134 | uploaded? 135 | 136 | Do the text files end in ".txt"? For documents to be recognized as 137 | text files to be uploaded, they have to have that file extension. 138 | *Unless* you have specified an alternate file extension to use in 139 | ".simplenotesyncrc". 140 | 141 | Text files can't be located in subdirectories - this script does not 142 | (by design) recurse folders looking for files (since they shouldn't 143 | be anywhere but the specified directory). 144 | 145 | * When my note is downloaded from Simplenote and then changed locally, I 146 | end up with two copies of the first line (one shorter than the other) - 147 | what gives? 148 | 149 | If the first line of a note is too long to become the filename, it 150 | is trimmed to an appropriate length. To prevent losing data, the 151 | full line is preserved in the body. Since Simplenote doesn't have a 152 | concept of titles, the title becomes the first line (which is 153 | trimmed), and the original first line is now the third line 154 | (counting the blank line in between). Your only alternatives are to 155 | shorten the first line, split it in two, or to create a short title 156 | 157 | * If I rename a note, what happens? 158 | 159 | If you rename a note on Simplenote by changing the first line, a new 160 | text file will be created and the old one will be deleted, 161 | preserving the original creation date. If you rename a text file 162 | locally, the old note on Simplenote will be deleted and a new one 163 | will be created, again preserving the original creation date. In the 164 | second instance, there is not actually any recognition of a "rename" 165 | going on - simply the recognition that an old note was deleted and a 166 | new one exists. 167 | 168 | 169 | # Troubleshooting # 170 | 171 | If SimplenoteSync isn't working, I've tried to add more (and better) 172 | error messages. Common problems so far include: 173 | 174 | * Not installing Crypt::SSLeay 175 | 176 | * Errors in the "simplenotesyncrc" file 177 | 178 | Optionally, you can enable or disable writing changes to either the 179 | local directory or to the Simplenote web server. For example, if you 180 | want to attempt to copy files to your computer without risking your 181 | remote data, you can disable "$allow_server_updates". Or, you can 182 | disable "$allow_local_updates" to protect your local data. 183 | 184 | Additionally, there is a script "Debug.pl" that will generate a text 185 | file with some useful information to email to me if you continue to have 186 | trouble. 187 | 188 | 189 | # Known Issues # 190 | 191 | * No merging when both local and remote file are changed between syncs - 192 | this might be enabled in the future 193 | 194 | * the code is still somewhat ugly 195 | 196 | * it's probably not very efficient and might really bog down with large 197 | numbers of notes 198 | 199 | * renaming notes or text files causes it to be treated as a new note - 200 | probably not all bad, but not sure what else to do. For now, you'll have 201 | to manually delete the old copy 202 | 203 | 204 | # See Also # 205 | 206 | Designed for use with Simplenote for iPhone: 207 | 208 | 209 | 210 | The SimplenoteSync homepage is: 211 | 212 | 213 | 214 | SimplenoteSync is available on github: 215 | 216 | 217 | 218 | A Discussion list is also available: 219 | 220 | 221 | 222 | 223 | # Author # 224 | 225 | Fletcher T. Penney, 226 | 227 | 228 | # Copyright And License # 229 | 230 | Copyright (C) 2009 by Fletcher T. Penney 231 | 232 | This program is free software; you can redistribute it and/or modify it 233 | under the terms of the GNU General Public License as published by the 234 | Free Software Foundation; either version 2 of the License, or (at your 235 | option) any later version. 236 | 237 | This program is distributed in the hope that it will be useful, but 238 | WITHOUT ANY WARRANTY; without even the implied warranty of 239 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 240 | Public License for more details. 241 | 242 | You should have received a copy of the GNU General Public License along 243 | with this program; if not, write to the Free Software Foundation, Inc. 244 | 59 Temple Place, Suite 330 Boston, MA 02111-1307 USA 245 | 246 | -------------------------------------------------------------------------------- /SimplenoteSync.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # 3 | # SimplenoteSync.pl 4 | # 5 | # Copyright (c) 2009 Fletcher T. Penney 6 | # 7 | # 8 | # 9 | 10 | # TODO: cache authentication token between runs 11 | # TODO: How to handle simultaneous edits? 12 | # TODO: need to compare information between local and remote files when same title in both (e.g. simplenotesync.db lost, or collision) 13 | # TODO: Windows compatibility?? This has not been tested AT ALL yet 14 | # TODO: Further testing on Linux - mainly file creation time 15 | 16 | use strict; 17 | use warnings; 18 | use File::Basename; 19 | use File::Path; 20 | use Cwd; 21 | use Cwd 'abs_path'; 22 | use MIME::Base64; 23 | use LWP::UserAgent; 24 | my $ua = LWP::UserAgent->new; 25 | use Time::Local; 26 | use File::Copy; 27 | use Encode 'decode_utf8'; 28 | use Encode; 29 | 30 | # Configuration 31 | # 32 | # Create file in your home directory named ".simplenotesyncrc" 33 | # First line is your email address 34 | # Second line is your Simplenote password 35 | # Third line is the directory to be used for text files 36 | 37 | open (CONFIG, "<$ENV{HOME}/.simplenotesyncrc") or die "Unable to load config file $ENV{HOME}/.simplenotesyncrc.\n"; 38 | 39 | my $email = ; 40 | my $password = ; 41 | my $rc_directory = ; 42 | my $file_extension = ; 43 | my $sync_directory; 44 | 45 | 46 | if (! defined ($file_extension)) { 47 | $file_extension = "txt"; 48 | } 49 | 50 | close CONFIG; 51 | chomp ($email, $password, $rc_directory, $file_extension); 52 | 53 | if ($rc_directory eq "") { 54 | # If a valid directory isn't specified, then don't keep going 55 | die "A directory was not specified.\n"; 56 | }; 57 | 58 | $rc_directory =~ s/\\ / /g; 59 | 60 | if ($sync_directory = abs_path($rc_directory)) { 61 | } else { 62 | # If a valid directory isn't specified, then don't keep going 63 | die "\"$rc_directory\" does not appear to be a valid directory.\n"; 64 | }; 65 | 66 | $file_extension =~ s/^\s*\.(.*?)\s*$/$1/; 67 | 68 | if ($file_extension =~ /^\s*$/) { 69 | $file_extension = "txt"; 70 | } 71 | 72 | my $url = 'https://simple-note.appspot.com/api/'; 73 | my $token; 74 | 75 | 76 | # Options 77 | my $debug = 1; # enable log messages for troubleshooting 78 | my $allow_local_updates = 1; # Allow changes to local text files 79 | my $allow_server_updates = 1; # Allow changes to Simplenote server 80 | my $store_base_text = 0; # Trial mode to allow conflict resolution 81 | my $flag_network_traffic = 0; # Print a warning for each network call 82 | 83 | # On which OS are we running? 84 | my $os = $^O; # Mac = darwin; Linux = linux; Windows contains MSWin 85 | 86 | # Initialize Database of last sync information into global array 87 | my $hash_ref = initSyncDatabase($sync_directory); 88 | my %syncNotes = %$hash_ref; 89 | 90 | 91 | # Initialize database of newly synchronized files 92 | my %newNotes = (); 93 | 94 | 95 | # Initialize database of files that were deleted this round 96 | my %deletedFromDatabase = (); 97 | 98 | 99 | # Get authorization token 100 | $token = getToken(); 101 | 102 | 103 | # Do Synchronization 104 | synchronizeNotesToFolder($sync_directory); 105 | 106 | 107 | # Write new database for next time 108 | writeSyncDatabase($sync_directory); 109 | 110 | 111 | 1; 112 | 113 | 114 | sub getToken { 115 | # Connect to server and get a authentication token 116 | 117 | my $content = encode_base64("email=$email&password=$password"); 118 | 119 | warn "Network: get token\n" if $flag_network_traffic; 120 | my $response = $ua->post($url . "login", Content => $content); 121 | 122 | if ($response->content =~ /Invalid argument/) { 123 | die "Problem connecting to web server.\nHave you installed Crypt:SSLeay as instructed?\n"; 124 | } 125 | 126 | die "Error logging into Simplenote server:\n$response->content\n" unless $response->is_success; 127 | 128 | return $response->content; 129 | } 130 | 131 | 132 | sub getNoteIndex { 133 | # Get list of notes from simplenote server 134 | my %note = (); 135 | 136 | warn "Network: get note index\n" if $flag_network_traffic; 137 | my $response = $ua->get($url . "index?auth=$token&email=$email"); 138 | my $index = $response->content; 139 | 140 | $index =~ s{ 141 | \{(.*?)\} 142 | }{ 143 | # iterate through notes in index and load into hash 144 | my $notedata = $1; 145 | 146 | $notedata =~ /"key":\s*"(.*?)"/; 147 | my $key = $1; 148 | 149 | while ($notedata =~ /"(.*?)":\s*"?(.*?)"?(,|\Z)/g) { 150 | # load note data into hash 151 | if ($1 ne "key") { 152 | $note{$key}{$1} = $2; 153 | } 154 | } 155 | 156 | # Trim fractions of seconds from modification time 157 | $note{$key}{modify} =~ s/\..*$//; 158 | }egx; 159 | 160 | return \%note; 161 | } 162 | 163 | 164 | sub titleToFilename { 165 | # Convert note's title into valid filename 166 | my $title = shift; 167 | 168 | # Strip prohibited characters 169 | $title =~ s/[:\\\/]/ /g; 170 | 171 | $title .= ".$file_extension"; 172 | 173 | return $title; 174 | } 175 | 176 | 177 | sub filenameToTitle { 178 | # Convert filename into title and unescape special characters 179 | my $filename = shift; 180 | 181 | $filename = basename ($filename); 182 | $filename =~ s/\.$file_extension$//; 183 | 184 | return $filename; 185 | } 186 | 187 | 188 | sub uploadFileToNote { 189 | # Given a local file, upload it as a note at simplenote web server 190 | my $filepath = shift; 191 | my $key = shift; # Supply key if we are updating existing note 192 | 193 | my $title = filenameToTitle($filepath); # The title for new note 194 | 195 | my $content = "\n"; # The content for new note 196 | open (INPUT, "<$filepath"); 197 | local $/; 198 | $content .= ; 199 | close(INPUT); 200 | 201 | # Check to make sure text file is encoded as UTF-8 202 | if (eval { decode_utf8($content, Encode::FB_CROAK); 1 }) { 203 | # $content is valid utf8 204 | } else { 205 | # $content is not valid utf8 - assume it's macroman and convert 206 | warn "$filepath is not a UTF-8 file. Will try to convert\n" if $debug; 207 | $content = decode('MacRoman', $content); 208 | utf8::encode($content); 209 | } 210 | 211 | my @d = gmtime ((stat("$filepath"))[9]); # get file's modification time 212 | my $modified = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 213 | 214 | if ($os =~ /darwin/i) { 215 | # The following works on Mac OS X - need a "birth time", not ctime 216 | @d = gmtime (readpipe ("stat -f \"%B\" \"$filepath\"")); # created time 217 | } else { 218 | # TODO: Need a better way to do this on non Mac systems 219 | @d = gmtime ((stat("$filepath"))[9]); # get file's modification time 220 | } 221 | 222 | my $created = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 223 | 224 | if (defined($key)) { 225 | # We are updating an old note 226 | 227 | my $modifyString = $modified ? "&modify=$modified" : ""; 228 | 229 | warn "Network: update existing note \"$title\"\n" if $flag_network_traffic; 230 | my $response = $ua->post($url . "note?key=$key&auth=$token&email=$email$modifyString", Content => encode_base64($title ."\n" . $content)) if ($allow_server_updates); 231 | } else { 232 | # We are creating a new note 233 | 234 | my $modifyString = $modified ? "&modify=$modified" : ""; 235 | my $createString = $created ? "&create=$created" : ""; 236 | 237 | warn "Network: create new note \"$title\"\n" if $flag_network_traffic; 238 | my $response = $ua->post($url . "note?auth=$token&email=$email$modifyString$createString", Content => encode_base64($title ."\n" . $content)) if ($allow_server_updates); 239 | 240 | # Return the key of the newly created note 241 | if ($allow_server_updates) { 242 | $key = $response->content; 243 | } else { 244 | $key = 0; 245 | } 246 | } 247 | 248 | # Add this note to the sync'ed list for writing to database 249 | $newNotes{$key}{modify} = $modified; 250 | $newNotes{$key}{create} = $created; 251 | $newNotes{$key}{title} = $title; 252 | $newNotes{$key}{file} = titleToFilename($title); 253 | 254 | if (($store_base_text) && ($allow_local_updates)) { 255 | # Put a copy of note in storage 256 | my $copy = dirname($filepath) . "/SimplenoteSync Storage/" . basename($filepath); 257 | copy($filepath,$copy); 258 | } 259 | 260 | return $key; 261 | } 262 | 263 | 264 | sub downloadNoteToFile { 265 | # Save local copy of note from Simplenote server 266 | my $key = shift; 267 | my $directory = shift; 268 | my $overwrite = shift; 269 | my $storage_directory = "$directory/SimplenoteSync Storage"; 270 | 271 | # retrieve note 272 | 273 | warn "Network: retrieve existing note \"$key\"\n" if $flag_network_traffic; 274 | my $response = $ua->get($url . "note?key=$key&auth=$token&email=$email&encode=base64"); 275 | my $content = decode_base64($response->content); 276 | 277 | if ($content eq "") { 278 | # No such note exists any longer 279 | warn "$key no longer exists on server\n"; 280 | $deletedFromDatabase{$key} = 1; 281 | return; 282 | } 283 | 284 | # Parse into title and content (if present) 285 | $content =~ s/^(.*?)(\n{1,2}|\Z)//s; # First line is title 286 | my $title = $1; 287 | my $divider = $2; 288 | 289 | # If first line is particularly long, it will get trimmed, so 290 | # leave it in body, and make a short version for the title 291 | if (length($title) > 240) { 292 | # Restore first line to content and create new title 293 | $content = $title . $divider . $content; 294 | $title = trimTitle($title); 295 | } 296 | 297 | my $filename = titleToFilename($title); 298 | 299 | # If note is marked for deletion on the server, don't download 300 | if ($response->header('note-deleted') eq "True" ) { 301 | if (($overwrite == 1) && ($allow_local_updates)) { 302 | # If we're in overwrite mode, then delete local copy 303 | File::Path::rmtree("$directory/$filename"); 304 | $deletedFromDatabase{$key} = 1; 305 | 306 | if ($store_base_text) { 307 | # Delete storage copy 308 | File::Path::rmtree("$storage_directory/$filename"); 309 | } 310 | } else { 311 | warn "note $key was flagged for deletion on server - not downloaded\n" if $debug; 312 | # Optionally, could add "&dead=1" to force Simplenote to remove 313 | # this note from the database. Could cause problems on iPhone 314 | # Just for future reference.... 315 | $deletedFromDatabase{$key} = 1; 316 | } 317 | return ""; 318 | } 319 | 320 | # Get time of note creation (trim fractions of seconds) 321 | my $create = my $createString = $response->header('note-createdate'); 322 | $create =~ /(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d):(\d\d)/; 323 | $create = timegm($6,$5,$4,$3,$2-1,$1); 324 | $createString =~ s/\..*$//; 325 | 326 | # Get time of note modification (trim fractions of seconds) 327 | my $modify = my $modifyString = $response->header('note-modifydate'); 328 | $modify =~ /(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d):(\d\d)/; 329 | $modify = timegm($6,$5,$4,$3,$2-1,$1); 330 | $modifyString =~ s/\..*$//; 331 | 332 | # Create new file 333 | 334 | if ((-f "$directory/$filename") && 335 | ($overwrite == 0)) { 336 | # A file already exists with that name, and we're not intentionally 337 | # replacing with a new copy. 338 | warn "$filename already exists. Will not download.\n"; 339 | 340 | return ""; 341 | } else { 342 | if ($allow_local_updates) { 343 | open (FILE, ">$directory/$filename"); 344 | print FILE $content; 345 | close FILE; 346 | 347 | if ($store_base_text) { 348 | # Put a copy in storage 349 | open (FILE, ">$storage_directory/$filename"); 350 | print FILE $content; 351 | close FILE; 352 | } 353 | 354 | # Set created and modified time 355 | # Not sure why this has to be done twice, but it seems to on Mac OS X 356 | utime $create, $create, "$directory/$filename"; 357 | utime $create, $modify, "$directory/$filename"; 358 | 359 | $newNotes{$key}{modify} = $modifyString; 360 | $newNotes{$key}{create} = $createString; 361 | $newNotes{$key}{file} = $filename; 362 | $newNotes{$key}{title} = $title; 363 | 364 | # Add this note to the sync'ed list for writing to database 365 | return $filename; 366 | } 367 | } 368 | 369 | return ""; 370 | } 371 | 372 | 373 | sub trimTitle { 374 | # If title is too long, it won't be a valid filename 375 | my $title = shift; 376 | 377 | $title =~ s/^(.{1,240}).*?$/$1/; 378 | $title =~ s/(.*)\s.*?$/$1/; # Try to trim at a word boundary 379 | 380 | return $title; 381 | } 382 | 383 | sub deleteNoteOnline { 384 | # Delete specified note from Simplenote server 385 | my $key = shift; 386 | 387 | if ($allow_server_updates) { 388 | warn "Network: delete note \"$key\"\n" if $flag_network_traffic; 389 | my $response = $ua->get($url . "delete?key=$key&auth=$token&email=$email"); 390 | return $response->content; 391 | } else { 392 | return ""; 393 | } 394 | } 395 | 396 | 397 | sub mergeConflicts{ 398 | # Both the local copy and server copy were changed since last sync 399 | # We'll merge the changes into a new master file, and flag any conflicts 400 | my $key = shift; 401 | 402 | 403 | } 404 | 405 | 406 | sub synchronizeNotesToFolder { 407 | # Main Synchronization routine 408 | my $directory = shift; 409 | $directory = abs_path($directory); # Clean up path 410 | 411 | if (! -d $directory) { 412 | # Target directory doesn't exist 413 | die "Destination directory \"$directory\" does not exist\n"; 414 | } 415 | 416 | my $storage_directory = "$directory/SimplenoteSync Storage"; 417 | if ((! -e $storage_directory) && $store_base_text) { 418 | # This directory saves a copy of the text at each successful sync 419 | # to allow three way merging 420 | mkdir $storage_directory; 421 | } 422 | 423 | # get list of existing notes from server with mod date and delete status 424 | my $note_ref = getNoteIndex(); 425 | my %note = %$note_ref; 426 | 427 | # get list of existing local text files with mod/creation date 428 | my %file = (); 429 | 430 | my $glob_directory = $directory; 431 | $glob_directory =~ s/ /\\ /g; 432 | 433 | foreach my $filepath (glob("$glob_directory/*.$file_extension")) { 434 | $filepath = abs_path($filepath); 435 | my @d=gmtime ((stat("$filepath"))[9]); 436 | $file{$filepath}{modify} = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 437 | 438 | if ($os =~ /darwin/i) { 439 | # The following works on Mac OS X - need a "birth time", not ctime 440 | # created time 441 | @d = gmtime (readpipe ("stat -f \"%B\" \"$filepath\"")); 442 | } else { 443 | # TODO: Need a better way to do this on non Mac systems 444 | # get file's modification time 445 | @d = gmtime ((stat("$filepath"))[9]); 446 | } 447 | 448 | $file{$filepath}{create} = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 449 | } 450 | 451 | # Iterate through sync database and assess current state of those files 452 | 453 | foreach my $key (keys %syncNotes) { 454 | # Cycle through each prior note from last sync 455 | my $last_mod_date = $syncNotes{$key}{modify}; 456 | my $filename = $syncNotes{$key}{file}; 457 | 458 | if (defined ($file{"$directory/$filename"})) { 459 | # the current item appears to exist as a local file 460 | print "$filename exists\n" if $debug; 461 | if ($file{"$directory/$filename"}{modify} eq $last_mod_date) { 462 | # file appears unchanged 463 | print "\tlocal copy unchanged\n" if $debug; 464 | 465 | if (defined ($note{$key}{modify})) { 466 | # Remote copy also exists 467 | print "\tremote copy exists\n" if $debug; 468 | 469 | if ($note{$key}{modify} eq $last_mod_date) { 470 | # note on server also appears unchanged 471 | 472 | # Nothing more to do 473 | } else { 474 | # note on server has changed, but local file hasn't 475 | print "\tremote file is changed\n" if $debug; 476 | 477 | if ($note{$key}{deleted} eq "true") { 478 | # Remote note was flagged for deletion 479 | warn "Deleting $filename as it was deleted on server\n"; 480 | if ($allow_local_updates) { 481 | File::Path::rmtree("$directory/$filename"); 482 | delete($file{"$directory/$filename"}); 483 | } 484 | } else { 485 | # Remote note not flagged for deletion 486 | # update local file and overwrite if necessary 487 | my $newFile = downloadNoteToFile($key,$directory,1); 488 | 489 | if (($newFile ne $filename) && ($newFile ne "")) { 490 | warn "Deleting $filename as it was renamed to $newFile\n"; 491 | # The file was renamed on server; delete old copy 492 | if ($allow_local_updates) { 493 | File::Path::rmtree("$directory/$filename"); 494 | delete($file{"$directory/$filename"}); 495 | } 496 | } 497 | } 498 | } 499 | 500 | # Remove this file from other queues 501 | delete($note{$key}); 502 | delete($file{"$directory/$filename"}); 503 | } else { 504 | # remote file is gone, delete local 505 | print "\tdelete $filename\n" if $debug; 506 | File::Path::rmtree("$directory/$filename") if ($allow_local_updates); 507 | $deletedFromDatabase{$key} = 1; 508 | delete($note{$key}); 509 | delete($file{"$directory/$filename"}); 510 | } 511 | } else { 512 | # local file appears changed 513 | print "\tlocal file has changed\n" if $debug; 514 | 515 | if ($note{$key}{modify} eq $last_mod_date) { 516 | # but note on server is old 517 | print "\tbut server copy is unchanged\n" if $debug; 518 | 519 | # update note on server 520 | uploadFileToNote("$directory/$filename",$key); 521 | 522 | # Remove this file from other queues 523 | delete($note{$key}); 524 | delete($file{"$directory/$filename"}); 525 | } else { 526 | # note on server has also changed 527 | warn "$filename was modified locally and on server - please check file for conflicts.\n"; 528 | 529 | # Use the stored copy from last sync to enable a three way 530 | # merge, then use this as the official copy and allow 531 | # user to manually edit any conflicts 532 | 533 | mergeConflicts($key); 534 | 535 | # Remove this file from other queues 536 | delete($note{$key}); 537 | delete($file{"$directory/$filename"}); 538 | } 539 | } 540 | } else { 541 | # no file exists - it must have been deleted locally 542 | 543 | if ($note{$key}{modify} eq $last_mod_date) { 544 | # note on server also appears unchanged 545 | 546 | # so we delete this file 547 | print "kill $filename\n" if $debug; 548 | deleteNoteOnline($key); 549 | 550 | 551 | # Remove this file from other queues 552 | delete($note{$key}); 553 | delete($file{"$directory/$filename"}); 554 | $deletedFromDatabase{$key} = 1; 555 | 556 | } else { 557 | # note on server has also changed 558 | 559 | if ($note{$key}{deleted} eq "true") { 560 | # note on server was deleted also 561 | print "delete $filename\n" if $debug; 562 | 563 | # Don't do anything locally 564 | delete($note{$key}); 565 | delete($file{"$directory/$filename"}); 566 | } else { 567 | warn "$filename deleted locally but modified on server\n"; 568 | 569 | # So, download from the server to resync, and 570 | # user must then re-delete if desired 571 | downloadNoteToFile($key,$directory,0); 572 | 573 | # Remove this file from other queues 574 | delete($note{$key}); 575 | delete($file{"$directory/$filename"}); 576 | } 577 | } 578 | } 579 | } 580 | 581 | # Now, we need to look at new notes on server and download 582 | 583 | foreach my $key (sort keys %note) { 584 | # Download, but don't overwrite existing file if present 585 | if ($note{$key}{deleted} ne "true") { 586 | downloadNoteToFile($key, $directory,0); 587 | } 588 | } 589 | 590 | # Finally, we need to look at new files locally and upload to server 591 | 592 | foreach my $new_file (sort keys %file) { 593 | print "new local file $new_file\n" if $debug; 594 | uploadFileToNote($new_file); 595 | } 596 | } 597 | 598 | 599 | sub initSyncDatabase{ 600 | # from 601 | 602 | my $directory = shift; 603 | my %synchronizedNotes = (); 604 | 605 | if (open (DB, "<$directory/simplenotesync.db")) { 606 | 607 | $/ = ""; # paragraph read mode 608 | while () { 609 | my @array = (); 610 | 611 | my @fields = split /^([^:]+):\s*/m; 612 | shift @fields; # for leading null field 613 | push(@array, { map /(.*)/, @fields }); 614 | 615 | for my $record (@array) { 616 | for my $key (sort keys %$record) { 617 | $synchronizedNotes{$record->{key}}{$key} = $record->{$key}; 618 | } 619 | } 620 | } 621 | 622 | close DB; 623 | } 624 | 625 | return \%synchronizedNotes; 626 | } 627 | 628 | 629 | sub writeSyncDatabase{ 630 | # from 631 | 632 | return 0 if (!$allow_local_updates); 633 | my ($directory) = @_; 634 | 635 | open (DB, ">$directory/simplenotesync.db"); 636 | 637 | foreach my $record (sort keys %newNotes) { 638 | for my $key (sort keys %{$newNotes{$record}}) { 639 | $syncNotes{$record}{$key} = ${$newNotes{$record}}{$key}; 640 | } 641 | } 642 | 643 | foreach my $key (sort keys %deletedFromDatabase) { 644 | delete($syncNotes{$key}); 645 | } 646 | 647 | foreach my $record (sort keys %syncNotes) { 648 | print DB "key: $record\n"; 649 | for my $key (sort keys %{$syncNotes{$record}}) { 650 | print DB "$key: ${$syncNotes{$record}}{$key}\n"; 651 | } 652 | print DB "\n"; 653 | } 654 | 655 | 656 | close DB; 657 | } 658 | 659 | 660 | =head1 NAME 661 | 662 | SimplenoteSync.pl --- synchronize a folder of text files with Simplenote. 663 | 664 | 665 | Of note, this software is not created by or endorsed by Cloud Factory, the 666 | creators of Simplenote, or anyone else for that matter. 667 | 668 | =head1 CONFIGURATION 669 | 670 | **UPDATE** --- Notational Velocity now has built in synchronizing with 671 | Simplenote. I have not fully tested it, and can't vouch for or against it's 672 | quality. But, for anyone who is using Simplenotesync just so that their NV 673 | notes and Simplenotes stay in sync, it is probably a *much* easier way to 674 | accomplish this. Most of the support questions I get are from people who are 675 | not very experienced with the command-line --- Notational Velocity's built in 676 | support requires nothing more than your Simplenote user name and password. 677 | Additionally, I am now primarily using WriteRoom, and am not actively working 678 | on SimplenoteSync anymore. For more information, please visit: 679 | 680 | 681 | 682 | 683 | **WARNING --- I am having an intermittent problem with the Simplenote server 684 | that causes files to be deleted intermittently. Please use with caution and 685 | backup your data** 686 | 687 | 688 | **BACKUP YOUR DATA BEFORE USING --- THIS PROJECT IS STILL BEING TESTED. IF YOU 689 | AREN'T CONFIDENT IN WHAT YOU'RE DOING, DON'T USE IT!!!!** 690 | 691 | Create file in your home directory named ".simplenotesyncrc" with the 692 | following contents: 693 | 694 | 1. First line is your email address 695 | 696 | 2. Second line is your Simplenote password 697 | 698 | 3. Third line is the directory to be used for text files 699 | 700 | 4. Fourth (optional line) is a file extension to use (defaults to "txt" if 701 | none specified) 702 | 703 | Unfortunately, you have to install Crypt::SSLeay to get https to work. You can 704 | do this by running the following command as an administrator: 705 | 706 | =over 707 | 708 | sudo perl -MCPAN -e "install Crypt::SSLeay" 709 | 710 | =back 711 | 712 | =head1 DESCRIPTION 713 | 714 | After specifying a folder to store local text files, and the email address and 715 | password associated with your Simplenote account, SimplenoteSync will attempt 716 | to synchronize the information in both places. 717 | 718 | Sync information is stored in "simplenotesync.db". If this file is lost, 719 | SimplenoteSync will have to attempt to look for "collisions" between local 720 | files and existing notes. When performing the first synchronization, it's best 721 | to start with an empty local folder (or an empty collection of notes on 722 | Simplenote), and then start adding files (or notes) afterwards. 723 | 724 | =head1 WARNING 725 | 726 | Please note that this software is still in development stages --- I STRONGLY 727 | urge you to backup all of your data before running to ensure nothing is lost. 728 | If you run SimplenoteSync on an empty local folder without a 729 | "simplenotesync.db" file, the net result will be to copy the remote notes to 730 | the local folder, effectively performing a backup. 731 | 732 | 733 | =head1 INSTALLATION 734 | 735 | Download the latest copy of SimplenoteSync.pl from github: 736 | 737 | 738 | 739 | =head1 FEATURES 740 | 741 | * Bidirectional synchronization between the Simplenote web site and a local 742 | directory of text files on your computer 743 | 744 | * Ability to upload notes to your iPhone without typing them by hand 745 | 746 | * Ability to backup the notes on your iPhone 747 | 748 | * Perform synchronizations automatically by using cron 749 | 750 | * Should handle unicode characters in title and content (works for me in some 751 | basic tests, but let me know if you have trouble) 752 | 753 | * The ability to manipulate your notes (via the local text files) using other 754 | applications (e.g. [Notational Velocity](http://notational.net/) if you use 755 | "Plain Text Files" for storage, shell scripts, AppleScript, 756 | [TaskPaper](http://www.hogbaysoftware.com/products/taskpaper), etc.) - 757 | you're limited only by your imagination 758 | 759 | * COMING SOON --- The ability to attempt to merge changes if a note is changed 760 | locally and on the server simultaneously 761 | 762 | =head1 LIMITATIONS 763 | 764 | * Certain characters are prohibited in filenames (:,\,/) - if present in the 765 | title, they are stripped out. 766 | 767 | * If the simplenotesync.db file is lost, SimplenoteSync.pl is currently unable 768 | to realize that a text file and a note represent the same object --- instead 769 | you should move your local text files, do a fresh sync to download all notes 770 | locally, and manually replace any missing notes. 771 | 772 | * Simplenote supports multiple notes with the same title, but two files cannot 773 | share the same filename. If you have two notes with the same title, only one 774 | will be downloaded. I suggest changing the title of the other note. 775 | 776 | 777 | =head1 FAQ 778 | 779 | * When I try to use SimplenoteSync, I get the following error: 780 | 781 | =over 782 | 783 | =over 784 | 785 | Network: get token 786 | 787 | Error logging into Simplenote server: 788 | 789 | HTTP::Response=HASH(0x1009b0110)->content 790 | 791 | =back 792 | 793 | The only time I have seen this error is when the username or password is 794 | entered into the configuration file incorrectly. Watch out for spaces at the 795 | end of lines. 796 | 797 | =back 798 | 799 | 800 | * Why can I download notes from Simplenote, but local notes aren't being 801 | uploaded? 802 | 803 | =over 804 | 805 | Do the text files end in ".txt"? For documents to be recognized as text files 806 | to be uploaded, they have to have that file extension. *Unless* you have 807 | specified an alternate file extension to use in ".simplenotesyncrc". 808 | 809 | Text files can't be located in subdirectories - this script does not (by 810 | design) recurse folders looking for files (since they shouldn't be anywhere 811 | but the specified directory). 812 | 813 | =back 814 | 815 | * When my note is downloaded from Simplenote and then changed locally, I end 816 | up with two copies of the first line (one shorter than the other) - what 817 | gives? 818 | 819 | =over 820 | 821 | If the first line of a note is too long to become the filename, it is trimmed 822 | to an appropriate length. To prevent losing data, the full line is preserved 823 | in the body. Since Simplenote doesn't have a concept of titles, the title 824 | becomes the first line (which is trimmed), and the original first line is now 825 | the third line (counting the blank line in between). Your only alternatives 826 | are to shorten the first line, split it in two, or to create a short title 827 | 828 | =back 829 | 830 | * If I rename a note, what happens? 831 | 832 | =over 833 | 834 | If you rename a note on Simplenote by changing the first line, a new text file 835 | will be created and the old one will be deleted, preserving the original 836 | creation date. If you rename a text file locally, the old note on Simplenote 837 | will be deleted and a new one will be created, again preserving the original 838 | creation date. In the second instance, there is not actually any recognition 839 | of a "rename" going on - simply the recognition that an old note was deleted 840 | and a new one exists. 841 | 842 | =back 843 | 844 | =head1 TROUBLESHOOTING 845 | 846 | If SimplenoteSync isn't working, I've tried to add more (and better) error 847 | messages. Common problems so far include: 848 | 849 | * Not installing Crypt::SSLeay 850 | 851 | * Errors in the "simplenotesyncrc" file 852 | 853 | Optionally, you can enable or disable writing changes to either the local 854 | directory or to the Simplenote web server. For example, if you want to attempt 855 | to copy files to your computer without risking your remote data, you can 856 | disable "$allow_server_updates". Or, you can disable "$allow_local_updates" to 857 | protect your local data. 858 | 859 | Additionally, there is a script "Debug.pl" that will generate a text file with 860 | some useful information to email to me if you continue to have trouble. 861 | 862 | =head1 KNOWN ISSUES 863 | 864 | * No merging when both local and remote file are changed between syncs - this 865 | might be enabled in the future 866 | 867 | * the code is still somewhat ugly 868 | 869 | * it's probably not very efficient and might really bog down with large 870 | numbers of notes 871 | 872 | * renaming notes or text files causes it to be treated as a new note - 873 | probably not all bad, but not sure what else to do. For now, you'll have to 874 | manually delete the old copy 875 | 876 | 877 | =head1 SEE ALSO 878 | 879 | Designed for use with Simplenote for iPhone: 880 | 881 | 882 | 883 | The SimplenoteSync homepage is: 884 | 885 | 886 | 887 | SimplenoteSync is available on github: 888 | 889 | 890 | 891 | A Discussion list is also available: 892 | 893 | 894 | 895 | =head1 AUTHOR 896 | 897 | Fletcher T. Penney, Eowner@fletcherpenney.netE 898 | 899 | =head1 COPYRIGHT AND LICENSE 900 | 901 | Copyright (C) 2009 by Fletcher T. Penney 902 | 903 | This program is free software; you can redistribute it and/or modify 904 | it under the terms of the GNU General Public License as published by 905 | the Free Software Foundation; either version 2 of the License, or 906 | (at your option) any later version. 907 | 908 | This program is distributed in the hope that it will be useful, 909 | but WITHOUT ANY WARRANTY; without even the implied warranty of 910 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 911 | GNU General Public License for more details. 912 | 913 | You should have received a copy of the GNU General Public License 914 | along with this program; if not, write to the 915 | Free Software Foundation, Inc. 916 | 59 Temple Place, Suite 330 917 | Boston, MA 02111-1307 USA 918 | 919 | =cut -------------------------------------------------------------------------------- /nuclear-option.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # 3 | # nuclear-option.pl 4 | # 5 | # Copyright (c) 2009 Fletcher T. Penney 6 | # 7 | # 8 | # 9 | 10 | # 11 | # *********************** WARNING ******************************* 12 | # 13 | # The sole purpose of this script is to delete *ALL* of the notes 14 | # in your Simplenote account. 15 | # 16 | # ALL of them... 17 | # 18 | # ***************************************************************** 19 | 20 | use strict; 21 | use warnings; 22 | use File::Basename; 23 | use File::Path; 24 | use Cwd; 25 | use Cwd 'abs_path'; 26 | use MIME::Base64; 27 | use LWP::UserAgent; 28 | my $ua = LWP::UserAgent->new; 29 | use Time::Local; 30 | use File::Copy; 31 | use Encode 'decode_utf8'; 32 | use Encode; 33 | 34 | # Configuration 35 | # 36 | # Create file in your home directory named ".simplenotesyncrc" 37 | # First line is your email address 38 | # Second line is your Simplenote password 39 | # Third line is the directory to be used for text files 40 | 41 | open (CONFIG, "<$ENV{HOME}/.simplenotesyncrc") or die "Unable to load config file $ENV{HOME}/.simplenotesyncrc.\n"; 42 | 43 | my $email = ; 44 | my $password = ; 45 | my $rc_directory = ; 46 | my $file_extension = ; 47 | my $sync_directory; 48 | 49 | 50 | if (! defined ($file_extension)) { 51 | $file_extension = "txt"; 52 | } 53 | 54 | close CONFIG; 55 | chomp ($email, $password, $rc_directory, $file_extension); 56 | 57 | if ($rc_directory eq "") { 58 | # If a valid directory isn't specified, then don't keep going 59 | die "A directory was not specified.\n"; 60 | }; 61 | 62 | $rc_directory =~ s/\\ / /g; 63 | 64 | if ($sync_directory = abs_path($rc_directory)) { 65 | } else { 66 | # If a valid directory isn't specified, then don't keep going 67 | die "\"$rc_directory\" does not appear to be a valid directory.\n"; 68 | }; 69 | 70 | $file_extension =~ s/^\s*\.(.*?)\s*$/$1/; 71 | 72 | if ($file_extension =~ /^\s*$/) { 73 | $file_extension = "txt"; 74 | } 75 | 76 | my $url = 'https://simple-note.appspot.com/api/'; 77 | my $token; 78 | 79 | 80 | # Options 81 | my $debug = 1; # enable log messages for troubleshooting 82 | my $allow_local_updates = 0; # Allow changes to local text files 83 | my $allow_server_updates = 1; # Allow changes to Simplenote server 84 | my $store_base_text = 0; # Trial mode to allow conflict resolution 85 | my $flag_network_traffic = 1; # Print a warning for each network call 86 | 87 | # On which OS are we running? 88 | my $os = $^O; # Mac = darwin; Linux = linux; Windows contains MSWin 89 | 90 | # Initialize Database of last sync information into global array 91 | my $hash_ref = initSyncDatabase($sync_directory); 92 | my %syncNotes = %$hash_ref; 93 | 94 | 95 | # Initialize database of newly synchronized files 96 | my %newNotes = (); 97 | 98 | 99 | # Initialize database of files that were deleted this round 100 | my %deletedFromDatabase = (); 101 | 102 | 103 | # Get authorization token 104 | $token = getToken(); 105 | 106 | 107 | # Do Synchronization 108 | deleteAllNotesOnServer(); 109 | #synchronizeNotesToFolder($sync_directory); 110 | 111 | 112 | # Write new database for next time 113 | writeSyncDatabase($sync_directory); 114 | 115 | 116 | 1; 117 | 118 | 119 | sub getToken { 120 | # Connect to server and get a authentication token 121 | 122 | my $content = encode_base64("email=$email&password=$password"); 123 | 124 | warn "Network: get token\n" if $flag_network_traffic; 125 | my $response = $ua->post($url . "login", Content => $content); 126 | 127 | if ($response->content =~ /Invalid argument/) { 128 | die "Problem connecting to web server.\nHave you installed Crypt:SSLeay as instructed?\n"; 129 | } 130 | 131 | die "Error logging into Simplenote server:\n$response->content\n" unless $response->is_success; 132 | 133 | return $response->content; 134 | } 135 | 136 | 137 | sub getNoteIndex { 138 | # Get list of notes from simplenote server 139 | my %note = (); 140 | 141 | warn "Network: get note index\n" if $flag_network_traffic; 142 | my $response = $ua->get($url . "index?auth=$token&email=$email"); 143 | my $index = $response->content; 144 | 145 | $index =~ s{ 146 | \{(.*?)\} 147 | }{ 148 | # iterate through notes in index and load into hash 149 | my $notedata = $1; 150 | 151 | $notedata =~ /"key":\s*"(.*?)"/; 152 | my $key = $1; 153 | 154 | while ($notedata =~ /"(.*?)":\s*"?(.*?)"?(,|\Z)/g) { 155 | # load note data into hash 156 | if ($1 ne "key") { 157 | $note{$key}{$1} = $2; 158 | } 159 | } 160 | 161 | # Trim fractions of seconds from modification time 162 | $note{$key}{modify} =~ s/\..*$//; 163 | }egx; 164 | 165 | return \%note; 166 | } 167 | 168 | 169 | sub titleToFilename { 170 | # Convert note's title into valid filename 171 | my $title = shift; 172 | 173 | # Strip prohibited characters 174 | $title =~ s/[:\\\/]/ /g; 175 | 176 | $title .= ".$file_extension"; 177 | 178 | return $title; 179 | } 180 | 181 | 182 | sub filenameToTitle { 183 | # Convert filename into title and unescape special characters 184 | my $filename = shift; 185 | 186 | $filename = basename ($filename); 187 | $filename =~ s/\.$file_extension$//; 188 | 189 | return $filename; 190 | } 191 | 192 | 193 | sub uploadFileToNote { 194 | # Given a local file, upload it as a note at simplenote web server 195 | my $filepath = shift; 196 | my $key = shift; # Supply key if we are updating existing note 197 | 198 | my $title = filenameToTitle($filepath); # The title for new note 199 | 200 | my $content = "\n"; # The content for new note 201 | open (INPUT, "<$filepath"); 202 | local $/; 203 | $content .= ; 204 | close(INPUT); 205 | 206 | # Check to make sure text file is encoded as UTF-8 207 | if (eval { decode_utf8($content, Encode::FB_CROAK); 1 }) { 208 | # $content is valid utf8 209 | } else { 210 | # $content is not valid utf8 - assume it's macroman and convert 211 | warn "$filepath is not a UTF-8 file. Will try to convert\n" if $debug; 212 | $content = decode('MacRoman', $content); 213 | utf8::encode($content); 214 | } 215 | 216 | my @d = gmtime ((stat("$filepath"))[9]); # get file's modification time 217 | my $modified = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 218 | 219 | if ($os =~ /darwin/i) { 220 | # The following works on Mac OS X - need a "birth time", not ctime 221 | @d = gmtime (readpipe ("stat -f \"%B\" \"$filepath\"")); # created time 222 | } else { 223 | # TODO: Need a better way to do this on non Mac systems 224 | @d = gmtime ((stat("$filepath"))[9]); # get file's modification time 225 | } 226 | 227 | my $created = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 228 | 229 | if (defined($key)) { 230 | # We are updating an old note 231 | 232 | my $modifyString = $modified ? "&modify=$modified" : ""; 233 | 234 | warn "Network: update existing note \"$title\"\n" if $flag_network_traffic; 235 | my $response = $ua->post($url . "note?key=$key&auth=$token&email=$email$modifyString", Content => encode_base64($title ."\n" . $content)) if ($allow_server_updates); 236 | } else { 237 | # We are creating a new note 238 | 239 | my $modifyString = $modified ? "&modify=$modified" : ""; 240 | my $createString = $created ? "&create=$created" : ""; 241 | 242 | warn "Network: create new note \"$title\"\n" if $flag_network_traffic; 243 | my $response = $ua->post($url . "note?auth=$token&email=$email$modifyString$createString", Content => encode_base64($title ."\n" . $content)) if ($allow_server_updates); 244 | 245 | # Return the key of the newly created note 246 | if ($allow_server_updates) { 247 | $key = $response->content; 248 | } else { 249 | $key = 0; 250 | } 251 | } 252 | 253 | # Add this note to the sync'ed list for writing to database 254 | $newNotes{$key}{modify} = $modified; 255 | $newNotes{$key}{create} = $created; 256 | $newNotes{$key}{title} = $title; 257 | $newNotes{$key}{file} = titleToFilename($title); 258 | 259 | if (($store_base_text) && ($allow_local_updates)) { 260 | # Put a copy of note in storage 261 | my $copy = dirname($filepath) . "/SimplenoteSync Storage/" . basename($filepath); 262 | copy($filepath,$copy); 263 | } 264 | 265 | return $key; 266 | } 267 | 268 | 269 | sub downloadNoteToFile { 270 | # Save local copy of note from Simplenote server 271 | my $key = shift; 272 | my $directory = shift; 273 | my $overwrite = shift; 274 | my $storage_directory = "$directory/SimplenoteSync Storage"; 275 | 276 | # retrieve note 277 | 278 | warn "Network: retrieve existing note \"$key\"\n" if $flag_network_traffic; 279 | my $response = $ua->get($url . "note?key=$key&auth=$token&email=$email&encode=base64"); 280 | my $content = decode_base64($response->content); 281 | 282 | if ($content eq "") { 283 | # No such note exists any longer 284 | warn "$key no longer exists on server\n"; 285 | $deletedFromDatabase{$key} = 1; 286 | return; 287 | } 288 | 289 | # Parse into title and content (if present) 290 | $content =~ s/^(.*?)(\n{1,2}|\Z)//s; # First line is title 291 | my $title = $1; 292 | my $divider = $2; 293 | 294 | # If first line is particularly long, it will get trimmed, so 295 | # leave it in body, and make a short version for the title 296 | if (length($title) > 240) { 297 | # Restore first line to content and create new title 298 | $content = $title . $divider . $content; 299 | $title = trimTitle($title); 300 | } 301 | 302 | my $filename = titleToFilename($title); 303 | 304 | # If note is marked for deletion on the server, don't download 305 | if ($response->header('note-deleted') eq "True" ) { 306 | if (($overwrite == 1) && ($allow_local_updates)) { 307 | # If we're in overwrite mode, then delete local copy 308 | File::Path::rmtree("$directory/$filename"); 309 | $deletedFromDatabase{$key} = 1; 310 | 311 | if ($store_base_text) { 312 | # Delete storage copy 313 | File::Path::rmtree("$storage_directory/$filename"); 314 | } 315 | } else { 316 | warn "note $key was flagged for deletion on server - not downloaded\n" if $debug; 317 | # Optionally, could add "&dead=1" to force Simplenote to remove 318 | # this note from the database. Could cause problems on iPhone 319 | # Just for future reference.... 320 | $deletedFromDatabase{$key} = 1; 321 | } 322 | return ""; 323 | } 324 | 325 | # Get time of note creation (trim fractions of seconds) 326 | my $create = my $createString = $response->header('note-createdate'); 327 | $create =~ /(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d):(\d\d)/; 328 | $create = timegm($6,$5,$4,$3,$2-1,$1); 329 | $createString =~ s/\..*$//; 330 | 331 | # Get time of note modification (trim fractions of seconds) 332 | my $modify = my $modifyString = $response->header('note-modifydate'); 333 | $modify =~ /(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d):(\d\d)/; 334 | $modify = timegm($6,$5,$4,$3,$2-1,$1); 335 | $modifyString =~ s/\..*$//; 336 | 337 | # Create new file 338 | 339 | if ((-f "$directory/$filename") && 340 | ($overwrite == 0)) { 341 | # A file already exists with that name, and we're not intentionally 342 | # replacing with a new copy. 343 | warn "$filename already exists. Will not download.\n"; 344 | 345 | return ""; 346 | } else { 347 | if ($allow_local_updates) { 348 | open (FILE, ">$directory/$filename"); 349 | print FILE $content; 350 | close FILE; 351 | 352 | if ($store_base_text) { 353 | # Put a copy in storage 354 | open (FILE, ">$storage_directory/$filename"); 355 | print FILE $content; 356 | close FILE; 357 | } 358 | 359 | # Set created and modified time 360 | # Not sure why this has to be done twice, but it seems to on Mac OS X 361 | utime $create, $create, "$directory/$filename"; 362 | utime $create, $modify, "$directory/$filename"; 363 | 364 | $newNotes{$key}{modify} = $modifyString; 365 | $newNotes{$key}{create} = $createString; 366 | $newNotes{$key}{file} = $filename; 367 | $newNotes{$key}{title} = $title; 368 | 369 | # Add this note to the sync'ed list for writing to database 370 | return $filename; 371 | } 372 | } 373 | 374 | return ""; 375 | } 376 | 377 | 378 | sub trimTitle { 379 | # If title is too long, it won't be a valid filename 380 | my $title = shift; 381 | 382 | $title =~ s/^(.{1,240}).*?$/$1/; 383 | $title =~ s/(.*)\s.*?$/$1/; # Try to trim at a word boundary 384 | 385 | return $title; 386 | } 387 | 388 | sub deleteNoteOnline { 389 | # Delete specified note from Simplenote server 390 | my $key = shift; 391 | 392 | if ($allow_server_updates) { 393 | warn "Network: delete note \"$key\"\n" if $flag_network_traffic; 394 | my $response = $ua->get($url . "delete?key=$key&auth=$token&email=$email&dead=1"); 395 | return $response->content; 396 | } else { 397 | return ""; 398 | } 399 | } 400 | 401 | 402 | sub mergeConflicts{ 403 | # Both the local copy and server copy were changed since last sync 404 | # We'll merge the changes into a new master file, and flag any conflicts 405 | my $key = shift; 406 | 407 | 408 | } 409 | 410 | 411 | sub synchronizeNotesToFolder { 412 | # Main Synchronization routine 413 | my $directory = shift; 414 | $directory = abs_path($directory); # Clean up path 415 | 416 | if (! -d $directory) { 417 | # Target directory doesn't exist 418 | die "Destination directory \"$directory\" does not exist\n"; 419 | } 420 | 421 | my $storage_directory = "$directory/SimplenoteSync Storage"; 422 | if ((! -e $storage_directory) && $store_base_text) { 423 | # This directory saves a copy of the text at each successful sync 424 | # to allow three way merging 425 | mkdir $storage_directory; 426 | } 427 | 428 | # get list of existing notes from server with mod date and delete status 429 | my $note_ref = getNoteIndex(); 430 | my %note = %$note_ref; 431 | 432 | # get list of existing local text files with mod/creation date 433 | my %file = (); 434 | 435 | my $glob_directory = $directory; 436 | $glob_directory =~ s/ /\\ /g; 437 | 438 | foreach my $filepath (glob("$glob_directory/*.$file_extension")) { 439 | $filepath = abs_path($filepath); 440 | my @d=gmtime ((stat("$filepath"))[9]); 441 | $file{$filepath}{modify} = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 442 | 443 | if ($os =~ /darwin/i) { 444 | # The following works on Mac OS X - need a "birth time", not ctime 445 | # created time 446 | @d = gmtime (readpipe ("stat -f \"%B\" \"$filepath\"")); 447 | } else { 448 | # TODO: Need a better way to do this on non Mac systems 449 | # get file's modification time 450 | @d = gmtime ((stat("$filepath"))[9]); 451 | } 452 | 453 | $file{$filepath}{create} = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]; 454 | } 455 | 456 | # Iterate through sync database and assess current state of those files 457 | 458 | foreach my $key (keys %syncNotes) { 459 | # Cycle through each prior note from last sync 460 | my $last_mod_date = $syncNotes{$key}{modify}; 461 | my $filename = $syncNotes{$key}{file}; 462 | 463 | if (defined ($file{"$directory/$filename"})) { 464 | # the current item appears to exist as a local file 465 | print "$filename exists\n" if $debug; 466 | if ($file{"$directory/$filename"}{modify} eq $last_mod_date) { 467 | # file appears unchanged 468 | print "\tlocal copy unchanged\n" if $debug; 469 | 470 | if (defined ($note{$key}{modify})) { 471 | # Remote copy also exists 472 | print "\tremote copy exists\n" if $debug; 473 | 474 | if ($note{$key}{modify} eq $last_mod_date) { 475 | # note on server also appears unchanged 476 | 477 | # Nothing more to do 478 | } else { 479 | # note on server has changed, but local file hasn't 480 | print "\tremote file is changed\n" if $debug; 481 | 482 | if ($note{$key}{deleted} eq "true") { 483 | # Remote note was flagged for deletion 484 | warn "Deleting $filename as it was deleted on server\n"; 485 | if ($allow_local_updates) { 486 | File::Path::rmtree("$directory/$filename"); 487 | delete($file{"$directory/$filename"}); 488 | } 489 | } else { 490 | # Remote note not flagged for deletion 491 | # update local file and overwrite if necessary 492 | my $newFile = downloadNoteToFile($key,$directory,1); 493 | 494 | if (($newFile ne $filename) && ($newFile ne "")) { 495 | warn "Deleting $filename as it was renamed to $newFile\n"; 496 | # The file was renamed on server; delete old copy 497 | if ($allow_local_updates) { 498 | File::Path::rmtree("$directory/$filename"); 499 | delete($file{"$directory/$filename"}); 500 | } 501 | } 502 | } 503 | } 504 | 505 | # Remove this file from other queues 506 | delete($note{$key}); 507 | delete($file{"$directory/$filename"}); 508 | } else { 509 | # remote file is gone, delete local 510 | print "\tdelete $filename\n" if $debug; 511 | File::Path::rmtree("$directory/$filename") if ($allow_local_updates); 512 | $deletedFromDatabase{$key} = 1; 513 | delete($note{$key}); 514 | delete($file{"$directory/$filename"}); 515 | } 516 | } else { 517 | # local file appears changed 518 | print "\tlocal file has changed\n" if $debug; 519 | 520 | if ($note{$key}{modify} eq $last_mod_date) { 521 | # but note on server is old 522 | print "\tbut server copy is unchanged\n" if $debug; 523 | 524 | # update note on server 525 | uploadFileToNote("$directory/$filename",$key); 526 | 527 | # Remove this file from other queues 528 | delete($note{$key}); 529 | delete($file{"$directory/$filename"}); 530 | } else { 531 | # note on server has also changed 532 | warn "$filename was modified locally and on server - please check file for conflicts.\n"; 533 | 534 | # Use the stored copy from last sync to enable a three way 535 | # merge, then use this as the official copy and allow 536 | # user to manually edit any conflicts 537 | 538 | mergeConflicts($key); 539 | 540 | # Remove this file from other queues 541 | delete($note{$key}); 542 | delete($file{"$directory/$filename"}); 543 | } 544 | } 545 | } else { 546 | # no file exists - it must have been deleted locally 547 | 548 | if ($note{$key}{modify} eq $last_mod_date) { 549 | # note on server also appears unchanged 550 | 551 | # so we delete this file 552 | print "kill $filename\n" if $debug; 553 | deleteNoteOnline($key); 554 | 555 | 556 | # Remove this file from other queues 557 | delete($note{$key}); 558 | delete($file{"$directory/$filename"}); 559 | $deletedFromDatabase{$key} = 1; 560 | 561 | } else { 562 | # note on server has also changed 563 | 564 | if ($note{$key}{deleted} eq "true") { 565 | # note on server was deleted also 566 | print "delete $filename\n" if $debug; 567 | 568 | # Don't do anything locally 569 | delete($note{$key}); 570 | delete($file{"$directory/$filename"}); 571 | } else { 572 | warn "$filename deleted locally but modified on server\n"; 573 | 574 | # So, download from the server to resync, and 575 | # user must then re-delete if desired 576 | downloadNoteToFile($key,$directory,0); 577 | 578 | # Remove this file from other queues 579 | delete($note{$key}); 580 | delete($file{"$directory/$filename"}); 581 | } 582 | } 583 | } 584 | } 585 | 586 | # Now, we need to look at new notes on server and download 587 | 588 | foreach my $key (sort keys %note) { 589 | # Download, but don't overwrite existing file if present 590 | if ($note{$key}{deleted} ne "true") { 591 | downloadNoteToFile($key, $directory,0); 592 | } 593 | } 594 | 595 | # Finally, we need to look at new files locally and upload to server 596 | 597 | foreach my $new_file (sort keys %file) { 598 | print "new local file $new_file\n" if $debug; 599 | uploadFileToNote($new_file); 600 | } 601 | } 602 | 603 | 604 | sub initSyncDatabase{ 605 | # from 606 | 607 | my $directory = shift; 608 | my %synchronizedNotes = (); 609 | 610 | if (open (DB, "<$directory/simplenotesync.db")) { 611 | 612 | $/ = ""; # paragraph read mode 613 | while () { 614 | my @array = (); 615 | 616 | my @fields = split /^([^:]+):\s*/m; 617 | shift @fields; # for leading null field 618 | push(@array, { map /(.*)/, @fields }); 619 | 620 | for my $record (@array) { 621 | for my $key (sort keys %$record) { 622 | $synchronizedNotes{$record->{key}}{$key} = $record->{$key}; 623 | } 624 | } 625 | } 626 | 627 | close DB; 628 | } 629 | 630 | return \%synchronizedNotes; 631 | } 632 | 633 | 634 | sub writeSyncDatabase{ 635 | # from 636 | 637 | return 0 if (!$allow_local_updates); 638 | my ($directory) = @_; 639 | 640 | open (DB, ">$directory/simplenotesync.db"); 641 | 642 | foreach my $record (sort keys %newNotes) { 643 | for my $key (sort keys %{$newNotes{$record}}) { 644 | $syncNotes{$record}{$key} = ${$newNotes{$record}}{$key}; 645 | } 646 | } 647 | 648 | foreach my $key (sort keys %deletedFromDatabase) { 649 | delete($syncNotes{$key}); 650 | } 651 | 652 | foreach my $record (sort keys %syncNotes) { 653 | print DB "key: $record\n"; 654 | for my $key (sort keys %{$syncNotes{$record}}) { 655 | print DB "$key: ${$syncNotes{$record}}{$key}\n"; 656 | } 657 | print DB "\n"; 658 | } 659 | 660 | 661 | close DB; 662 | } 663 | 664 | 665 | sub deleteAllNotesOnServer { 666 | # get list of existing notes from server with mod date and delete status 667 | my $note_ref = getNoteIndex(); 668 | my %note = %$note_ref; 669 | 670 | my $count = keys %note; 671 | my $counter = 1; 672 | foreach my $key (sort keys %note) { 673 | print "deleting $counter of $count\n"; 674 | deleteNoteOnline($key); 675 | $counter++; 676 | } 677 | } 678 | 679 | 680 | =head1 NAME 681 | 682 | SimplenoteSync.pl --- synchronize a folder of text files with Simplenote. 683 | 684 | 685 | Of note, this software is not created by or endorsed by Cloud Factory, the 686 | creators of Simplenote, or anyone else for that matter. 687 | 688 | =head1 CONFIGURATION 689 | 690 | **UPDATE** --- Notational Velocity now has built in synchronizing with 691 | Simplenote. I have not fully tested it, and can't vouch for or against it's 692 | quality. But, for anyone who is using Simplenotesync just so that their NV 693 | notes and Simplenotes stay in sync, it is probably a *much* easier way to 694 | accomplish this. Most of the support questions I get are from people who are 695 | not very experienced with the command-line --- Notational Velocity's built in 696 | support requires nothing more than your Simplenote user name and password. 697 | Additionally, I am now primarily using WriteRoom, and am not actively working 698 | on SimplenoteSync anymore. For more information, please visit: 699 | 700 | 701 | 702 | 703 | **WARNING --- I am having an intermittent problem with the Simplenote server 704 | that causes files to be deleted intermittently. Please use with caution and 705 | backup your data** 706 | 707 | 708 | **BACKUP YOUR DATA BEFORE USING --- THIS PROJECT IS STILL BEING TESTED. IF YOU 709 | AREN'T CONFIDENT IN WHAT YOU'RE DOING, DON'T USE IT!!!!** 710 | 711 | Create file in your home directory named ".simplenotesyncrc" with the 712 | following contents: 713 | 714 | 1. First line is your email address 715 | 716 | 2. Second line is your Simplenote password 717 | 718 | 3. Third line is the directory to be used for text files 719 | 720 | 4. Fourth (optional line) is a file extension to use (defaults to "txt" if 721 | none specified) 722 | 723 | Unfortunately, you have to install Crypt::SSLeay to get https to work. You can 724 | do this by running the following command as an administrator: 725 | 726 | =over 727 | 728 | sudo perl -MCPAN -e "install Crypt::SSLeay" 729 | 730 | =back 731 | 732 | =head1 DESCRIPTION 733 | 734 | After specifying a folder to store local text files, and the email address and 735 | password associated with your Simplenote account, SimplenoteSync will attempt 736 | to synchronize the information in both places. 737 | 738 | Sync information is stored in "simplenotesync.db". If this file is lost, 739 | SimplenoteSync will have to attempt to look for "collisions" between local 740 | files and existing notes. When performing the first synchronization, it's best 741 | to start with an empty local folder (or an empty collection of notes on 742 | Simplenote), and then start adding files (or notes) afterwards. 743 | 744 | =head1 WARNING 745 | 746 | Please note that this software is still in development stages --- I STRONGLY 747 | urge you to backup all of your data before running to ensure nothing is lost. 748 | If you run SimplenoteSync on an empty local folder without a 749 | "simplenotesync.db" file, the net result will be to copy the remote notes to 750 | the local folder, effectively performing a backup. 751 | 752 | 753 | =head1 INSTALLATION 754 | 755 | Download the latest copy of SimplenoteSync.pl from github: 756 | 757 | 758 | 759 | =head1 FEATURES 760 | 761 | * Bidirectional synchronization between the Simplenote web site and a local 762 | directory of text files on your computer 763 | 764 | * Ability to upload notes to your iPhone without typing them by hand 765 | 766 | * Ability to backup the notes on your iPhone 767 | 768 | * Perform synchronizations automatically by using cron 769 | 770 | * Should handle unicode characters in title and content (works for me in some 771 | basic tests, but let me know if you have trouble) 772 | 773 | * The ability to manipulate your notes (via the local text files) using other 774 | applications (e.g. [Notational Velocity](http://notational.net/) if you use 775 | "Plain Text Files" for storage, shell scripts, AppleScript, 776 | [TaskPaper](http://www.hogbaysoftware.com/products/taskpaper), etc.) - 777 | you're limited only by your imagination 778 | 779 | * COMING SOON --- The ability to attempt to merge changes if a note is changed 780 | locally and on the server simultaneously 781 | 782 | =head1 LIMITATIONS 783 | 784 | * Certain characters are prohibited in filenames (:,\,/) - if present in the 785 | title, they are stripped out. 786 | 787 | * If the simplenotesync.db file is lost, SimplenoteSync.pl is currently unable 788 | to realize that a text file and a note represent the same object --- instead 789 | you should move your local text files, do a fresh sync to download all notes 790 | locally, and manually replace any missing notes. 791 | 792 | * Simplenote supports multiple notes with the same title, but two files cannot 793 | share the same filename. If you have two notes with the same title, only one 794 | will be downloaded. I suggest changing the title of the other note. 795 | 796 | 797 | =head1 FAQ 798 | 799 | * When I try to use SimplenoteSync, I get the following error: 800 | 801 | =over 802 | 803 | =over 804 | 805 | Network: get token 806 | 807 | Error logging into Simplenote server: 808 | 809 | HTTP::Response=HASH(0x1009b0110)->content 810 | 811 | =back 812 | 813 | The only time I have seen this error is when the username or password is 814 | entered into the configuration file incorrectly. Watch out for spaces at the 815 | end of lines. 816 | 817 | =back 818 | 819 | 820 | * Why can I download notes from Simplenote, but local notes aren't being 821 | uploaded? 822 | 823 | =over 824 | 825 | Do the text files end in ".txt"? For documents to be recognized as text files 826 | to be uploaded, they have to have that file extension. *Unless* you have 827 | specified an alternate file extension to use in ".simplenotesyncrc". 828 | 829 | Text files can't be located in subdirectories - this script does not (by 830 | design) recurse folders looking for files (since they shouldn't be anywhere 831 | but the specified directory). 832 | 833 | =back 834 | 835 | * When my note is downloaded from Simplenote and then changed locally, I end 836 | up with two copies of the first line (one shorter than the other) - what 837 | gives? 838 | 839 | =over 840 | 841 | If the first line of a note is too long to become the filename, it is trimmed 842 | to an appropriate length. To prevent losing data, the full line is preserved 843 | in the body. Since Simplenote doesn't have a concept of titles, the title 844 | becomes the first line (which is trimmed), and the original first line is now 845 | the third line (counting the blank line in between). Your only alternatives 846 | are to shorten the first line, split it in two, or to create a short title 847 | 848 | =back 849 | 850 | * If I rename a note, what happens? 851 | 852 | =over 853 | 854 | If you rename a note on Simplenote by changing the first line, a new text file 855 | will be created and the old one will be deleted, preserving the original 856 | creation date. If you rename a text file locally, the old note on Simplenote 857 | will be deleted and a new one will be created, again preserving the original 858 | creation date. In the second instance, there is not actually any recognition 859 | of a "rename" going on - simply the recognition that an old note was deleted 860 | and a new one exists. 861 | 862 | =back 863 | 864 | =head1 TROUBLESHOOTING 865 | 866 | If SimplenoteSync isn't working, I've tried to add more (and better) error 867 | messages. Common problems so far include: 868 | 869 | * Not installing Crypt::SSLeay 870 | 871 | * Errors in the "simplenotesyncrc" file 872 | 873 | Optionally, you can enable or disable writing changes to either the local 874 | directory or to the Simplenote web server. For example, if you want to attempt 875 | to copy files to your computer without risking your remote data, you can 876 | disable "$allow_server_updates". Or, you can disable "$allow_local_updates" to 877 | protect your local data. 878 | 879 | Additionally, there is a script "Debug.pl" that will generate a text file with 880 | some useful information to email to me if you continue to have trouble. 881 | 882 | =head1 KNOWN ISSUES 883 | 884 | * No merging when both local and remote file are changed between syncs - this 885 | might be enabled in the future 886 | 887 | * the code is still somewhat ugly 888 | 889 | * it's probably not very efficient and might really bog down with large 890 | numbers of notes 891 | 892 | * renaming notes or text files causes it to be treated as a new note - 893 | probably not all bad, but not sure what else to do. For now, you'll have to 894 | manually delete the old copy 895 | 896 | 897 | =head1 SEE ALSO 898 | 899 | Designed for use with Simplenote for iPhone: 900 | 901 | 902 | 903 | The SimplenoteSync homepage is: 904 | 905 | 906 | 907 | SimplenoteSync is available on github: 908 | 909 | 910 | 911 | A Discussion list is also available: 912 | 913 | 914 | 915 | =head1 AUTHOR 916 | 917 | Fletcher T. Penney, Eowner@fletcherpenney.netE 918 | 919 | =head1 COPYRIGHT AND LICENSE 920 | 921 | Copyright (C) 2009 by Fletcher T. Penney 922 | 923 | This program is free software; you can redistribute it and/or modify 924 | it under the terms of the GNU General Public License as published by 925 | the Free Software Foundation; either version 2 of the License, or 926 | (at your option) any later version. 927 | 928 | This program is distributed in the hope that it will be useful, 929 | but WITHOUT ANY WARRANTY; without even the implied warranty of 930 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 931 | GNU General Public License for more details. 932 | 933 | You should have received a copy of the GNU General Public License 934 | along with this program; if not, write to the 935 | Free Software Foundation, Inc. 936 | 59 Temple Place, Suite 330 937 | Boston, MA 02111-1307 USA 938 | 939 | =cut --------------------------------------------------------------------------------