abbu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +39 -0
- data/LICENSE +21 -0
- data/README.md +132 -0
- data/bin/abbu +93 -0
- data/bin/cleanse +7 -0
- data/bin/console +10 -0
- data/bin/dev +7 -0
- data/bin/lint +7 -0
- data/bin/outdated +7 -0
- data/bin/test +7 -0
- data/docs/ABBU.md +87 -0
- data/examples/deduplicate_contacts.rb +25 -0
- data/examples/export_to_api.rb +27 -0
- data/examples/export_to_csv.rb +22 -0
- data/examples/export_to_json.rb +18 -0
- data/examples/export_to_vcard.rb +17 -0
- data/examples/stats_report.rb +18 -0
- data/examples/sync_to_crm.rb +22 -0
- data/lib/abbu/archive.rb +47 -0
- data/lib/abbu/contact.rb +26 -0
- data/lib/abbu/exporters/csv_exporter.rb +38 -0
- data/lib/abbu/exporters/json_exporter.rb +35 -0
- data/lib/abbu/exporters/vcard_exporter.rb +37 -0
- data/lib/abbu/parsers/plist_parser.rb +17 -0
- data/lib/abbu/parsers/sqlite_parser.rb +59 -0
- data/lib/abbu/utils/deduplicator.rb +19 -0
- data/lib/abbu/version.rb +6 -0
- data/lib/abbu.rb +18 -0
- data/tasks/abbu.rake +49 -0
- metadata +234 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4e7f9f82489460fb204dd66e345d8ce69e3a81944a9568cb81ae2dc84b41ed0e
|
|
4
|
+
data.tar.gz: 379defbf4b95f173d6afeaa255787e9f4a051562edf4428626f9048c32c3af66
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 11c08d30dbf68e633f072784e6d9a505bebc1dea90b3d527ec5472425588fe169dacb81b960469af9bd431b74b457ac9ef7ac637cad7357cd2cf25c9c1417f1f
|
|
7
|
+
data.tar.gz: 1eaf8b0d1b0733f4cb53fcbc6fe5dc11d0725acbb96878ec7677e1830501550bad2a035cf37c2c66c5e2b3da81576a10f0a3498192d0481c46deed5c8a7aacb5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!-- CHANGELOG.md -->
|
|
2
|
+
|
|
3
|
+
# Changelog
|
|
4
|
+
|
|
5
|
+
All notable changes to `abbu` are documented here.
|
|
6
|
+
|
|
7
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
8
|
+
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.1.0] - 2026-04-12
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `Abbu.open(path)` entry point returning an `Archive`
|
|
19
|
+
- `Archive#contacts` — reads contacts from SQLite or falls back to plist stub
|
|
20
|
+
- `Archive#sqlite?` — detects modern `.abcddb` bundles
|
|
21
|
+
- `Contact` object with `first_name`, `last_name`, `emails`, `phones`, `company`, `full_name`
|
|
22
|
+
- `Parsers::SqliteParser` — queries `ZABCDRECORD`, `ZABCDEMAILADDRESS`, `ZABCDPHONENUMBER`
|
|
23
|
+
- `Parsers::PlistParser` — stub with warning (legacy `.abcdp` support in v0.2)
|
|
24
|
+
- `Exporters::CsvExporter` — `to_file` and `to_stdout`
|
|
25
|
+
- `Exporters::JsonExporter` — `to_file` and `to_stdout`
|
|
26
|
+
- `Exporters::VcardExporter` — `to_file` and `to_stdout` (vCard 3.0)
|
|
27
|
+
- `Utils::Deduplicator` — groups contacts by first email, returns duplicates hash
|
|
28
|
+
- `bin/abbu` CLI with `--format`, `--output`, `--stats`, `--dedupe`, `--version`
|
|
29
|
+
- Rake tasks: `abbu:export`, `abbu:dedupe`, `abbu:stats`
|
|
30
|
+
- Example scripts: CSV, JSON, vCard, API, CRM sync, stats, dedupe
|
|
31
|
+
- `docs/ABBU.md` — file format reference
|
|
32
|
+
- RSpec test suite with 100% coverage target
|
|
33
|
+
- Guard + RuboCop DX loop
|
|
34
|
+
- GitHub Actions CI (Ruby 3.2 + 3.3)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
Stan Carver II
|
|
38
|
+
Made in Texas 🤠
|
|
39
|
+
https://stancarver.com
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stan Carver II
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!-- README.md -->
|
|
2
|
+
|
|
3
|
+
# abbu
|
|
4
|
+
|
|
5
|
+
Read and process Apple Contacts `.abbu` archives in Ruby.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Parse ABBU (Apple Contacts export) bundles
|
|
10
|
+
- SQLite-backed contact extraction (modern macOS)
|
|
11
|
+
- Legacy plist format detection (stub, v0.2 roadmap)
|
|
12
|
+
- Export to CSV, JSON, vCard
|
|
13
|
+
- CLI + Ruby API
|
|
14
|
+
- Duplicate detection
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gem install abbu
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or add to your `Gemfile`:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "abbu"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Ruby API
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
require "abbu"
|
|
34
|
+
|
|
35
|
+
archive = Abbu.open("Contacts.abbu")
|
|
36
|
+
contacts = archive.contacts
|
|
37
|
+
|
|
38
|
+
contacts.first.full_name # => "Stan Carver"
|
|
39
|
+
contacts.first.emails # => ["stan@example.com"]
|
|
40
|
+
contacts.first.phones # => ["555-1234"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Export
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# CSV
|
|
47
|
+
Abbu::Exporters::CsvExporter.new(archive.contacts).to_file("contacts.csv")
|
|
48
|
+
|
|
49
|
+
# JSON
|
|
50
|
+
Abbu::Exporters::JsonExporter.new(archive.contacts).to_file("contacts.json")
|
|
51
|
+
|
|
52
|
+
# vCard
|
|
53
|
+
Abbu::Exporters::VcardExporter.new(archive.contacts).to_file("contacts.vcf")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Duplicate Detection
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
dupes = Abbu::Utils::Deduplicator.new(archive.contacts).duplicates
|
|
60
|
+
dupes.each do |email, contacts|
|
|
61
|
+
puts "Duplicate: #{email}"
|
|
62
|
+
contacts.each { |c| puts " - #{c.full_name}" }
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Export to CSV
|
|
70
|
+
abbu Contacts.abbu -f csv -o contacts.csv
|
|
71
|
+
|
|
72
|
+
# JSON to stdout (pipeable)
|
|
73
|
+
abbu Contacts.abbu -f json | jq .
|
|
74
|
+
|
|
75
|
+
# vCard export
|
|
76
|
+
abbu Contacts.abbu -f vcard -o contacts.vcf
|
|
77
|
+
|
|
78
|
+
# Stats
|
|
79
|
+
abbu Contacts.abbu --stats
|
|
80
|
+
|
|
81
|
+
# Find duplicates
|
|
82
|
+
abbu Contacts.abbu --dedupe
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Rake Tasks
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# In your Rakefile:
|
|
89
|
+
load "tasks/abbu.rake"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
rake abbu:export[Contacts.abbu]
|
|
94
|
+
rake abbu:dedupe[Contacts.abbu]
|
|
95
|
+
rake abbu:stats[Contacts.abbu]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ABBU File Format
|
|
99
|
+
|
|
100
|
+
See [`docs/ABBU.md`](docs/ABBU.md) for a full explanation of the archive structure,
|
|
101
|
+
SQLite table schema, and format history.
|
|
102
|
+
|
|
103
|
+
## Roadmap
|
|
104
|
+
|
|
105
|
+
| Version | Features |
|
|
106
|
+
|---------|---------------------------------------------|
|
|
107
|
+
| v0.1.0 | SQLite parsing, CSV/JSON/vCard export, CLI |
|
|
108
|
+
| v0.2.0 | Plist parser, image extraction |
|
|
109
|
+
| v0.3.0 | Fuzzy dedupe (Levenshtein), merge engine |
|
|
110
|
+
| v1.0.0 | Sync adapters (Printavo, HubSpot, CRM) |
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
mise exec -- bundle install
|
|
116
|
+
mise exec -- bundle exec guard # DX loop: auto-test + auto-lint
|
|
117
|
+
mise exec -- bundle exec rspec # run specs
|
|
118
|
+
mise exec -- bundle exec rubocop # lint
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Contributing
|
|
122
|
+
|
|
123
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT. See [LICENSE](LICENSE).
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
Stan Carver II
|
|
131
|
+
Made in Texas 🤠
|
|
132
|
+
https://stancarver.com
|
data/bin/abbu
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# bin/abbu
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
+
|
|
7
|
+
require 'optparse'
|
|
8
|
+
require 'abbu'
|
|
9
|
+
|
|
10
|
+
options = {
|
|
11
|
+
format: nil,
|
|
12
|
+
output: nil,
|
|
13
|
+
dedupe: false,
|
|
14
|
+
stats: false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parser = OptionParser.new do |opts|
|
|
18
|
+
opts.banner = 'Usage: abbu <file.abbu> [options]'
|
|
19
|
+
|
|
20
|
+
opts.on('-f', '--format FORMAT', %w[csv json vcard], 'Export format (csv, json, vcard)') do |f|
|
|
21
|
+
options[:format] = f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
opts.on('-o', '--output FILE', 'Output file (default: stdout)') do |o|
|
|
25
|
+
options[:output] = o
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on('--stats', 'Print contact statistics') do
|
|
29
|
+
options[:stats] = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
opts.on('--dedupe', 'Find and print duplicate contacts') do
|
|
33
|
+
options[:dedupe] = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opts.on('-v', '--version', 'Print version') do
|
|
37
|
+
puts "abbu #{Abbu::VERSION}"
|
|
38
|
+
exit
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on('-h', '--help', 'Print this help') do
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
parser.parse!
|
|
48
|
+
|
|
49
|
+
file = ARGV.shift
|
|
50
|
+
|
|
51
|
+
if file.nil?
|
|
52
|
+
puts parser
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
archive = Abbu.open(file)
|
|
57
|
+
|
|
58
|
+
if options[:stats]
|
|
59
|
+
contacts = archive.contacts
|
|
60
|
+
puts "Total contacts : #{contacts.count}"
|
|
61
|
+
puts "With email : #{contacts.count { |c| c.emails.any? }}"
|
|
62
|
+
puts "With phone : #{contacts.count { |c| c.phones.any? }}"
|
|
63
|
+
exit
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if options[:dedupe]
|
|
67
|
+
require 'abbu/utils/deduplicator'
|
|
68
|
+
dupes = Abbu::Utils::Deduplicator.new(archive.contacts).duplicates
|
|
69
|
+
if dupes.empty?
|
|
70
|
+
puts 'No duplicates found.'
|
|
71
|
+
else
|
|
72
|
+
dupes.each do |email, contacts|
|
|
73
|
+
puts "Duplicate: #{email}"
|
|
74
|
+
contacts.each { |c| puts " - #{c.full_name}" }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
exit
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
case options[:format]
|
|
81
|
+
when 'csv'
|
|
82
|
+
exporter = Abbu::Exporters::CsvExporter.new(archive.contacts)
|
|
83
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
84
|
+
when 'json'
|
|
85
|
+
exporter = Abbu::Exporters::JsonExporter.new(archive.contacts)
|
|
86
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
87
|
+
when 'vcard'
|
|
88
|
+
exporter = Abbu::Exporters::VcardExporter.new(archive.contacts)
|
|
89
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
90
|
+
else
|
|
91
|
+
puts parser
|
|
92
|
+
exit 1
|
|
93
|
+
end
|
data/bin/cleanse
ADDED
data/bin/console
ADDED
data/bin/dev
ADDED
data/bin/lint
ADDED
data/bin/outdated
ADDED
data/bin/test
ADDED
data/docs/ABBU.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!-- docs/ABBU.md -->
|
|
2
|
+
|
|
3
|
+
# ABBU File Format (Apple Contacts Archive)
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`.abbu` files are exported from Apple Contacts.app and represent a full address book archive.
|
|
8
|
+
|
|
9
|
+
They are **not** a single file format — they are a macOS "package" (a directory bundle that Finder
|
|
10
|
+
presents as a single file). This means you can inspect the contents with `ls` or `open -a Finder`.
|
|
11
|
+
|
|
12
|
+
## Structure
|
|
13
|
+
|
|
14
|
+
Typical contents of a `.abbu` bundle:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Contacts.abbu/
|
|
18
|
+
├── AddressBook-v22.abcddb ← SQLite database for "Local" contacts (often mostly empty)
|
|
19
|
+
├── Metadata/ ← plist files (bundle metadata)
|
|
20
|
+
│ └── *.abcdp
|
|
21
|
+
├── Images/ ← contact photos (JPEG/PNG)
|
|
22
|
+
│ └── <uuid>.jpg
|
|
23
|
+
├── Sources/ ← Remote synced accounts (iCloud, Exchange, Google)
|
|
24
|
+
│ ├── <account_uuid>/
|
|
25
|
+
│ │ ├── AddressBook-v22.abcddb ← SQLite database for this specific account
|
|
26
|
+
│ │ ├── Metadata/
|
|
27
|
+
│ │ └── Images/
|
|
28
|
+
│ └── <another_uuid>/...
|
|
29
|
+
└── Records/ ← legacy plist-based contact records (older macOS)
|
|
30
|
+
└── <uuid>.abcdp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **Note:** The most common pitfall when parsing `.abbu` files is only reading the root `AddressBook-v22.abcddb`. For users syncing via iCloud or Exchange, the root database will be nearly empty. Parsers must recursively scan the `Sources/` directory to discover and extract all contacts from all `.abcddb` files.
|
|
34
|
+
|
|
35
|
+
## Formats
|
|
36
|
+
|
|
37
|
+
### 1. SQLite (modern macOS)
|
|
38
|
+
|
|
39
|
+
Newer macOS versions store the address book in a single SQLite database:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
AddressBook-v22.abcddb
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Key tables:
|
|
46
|
+
|
|
47
|
+
| Table | Purpose |
|
|
48
|
+
|------------------------|--------------------------------------|
|
|
49
|
+
| `ZABCDRECORD` | One row per contact (name, company) |
|
|
50
|
+
| `ZABCDEMAILADDRESS` | Email addresses (linked by `ZOWNER`) |
|
|
51
|
+
| `ZABCDPHONENUMBER` | Phone numbers (linked by `ZOWNER`) |
|
|
52
|
+
|
|
53
|
+
Notable columns in `ZABCDRECORD`:
|
|
54
|
+
|
|
55
|
+
| Column | Description |
|
|
56
|
+
|-----------------|------------------|
|
|
57
|
+
| `Z_PK` | Primary key |
|
|
58
|
+
| `ZFIRSTNAME` | First name |
|
|
59
|
+
| `ZLASTNAME` | Last name |
|
|
60
|
+
| `ZORGANIZATION` | Company / org |
|
|
61
|
+
|
|
62
|
+
### 2. Plist / `.abcdp` (legacy macOS)
|
|
63
|
+
|
|
64
|
+
Older macOS versions stored each contact as a separate binary plist file under `Records/`.
|
|
65
|
+
Each file is a serialised `ABPerson` dictionary. The `abbu` gem currently stubs this parser
|
|
66
|
+
and returns an empty array with a warning.
|
|
67
|
+
|
|
68
|
+
## Export Steps
|
|
69
|
+
|
|
70
|
+
To create a `.abbu` file:
|
|
71
|
+
|
|
72
|
+
1. Open **Contacts.app** on macOS
|
|
73
|
+
2. Select all contacts (`⌘A`)
|
|
74
|
+
3. File → Export → **Export vCard** *(or)* File → Export → **Contacts Archive…**
|
|
75
|
+
|
|
76
|
+
The "Contacts Archive" option produces a `.abbu` bundle.
|
|
77
|
+
|
|
78
|
+
## References
|
|
79
|
+
|
|
80
|
+
- [Apple Contacts Framework (private)](https://developer.apple.com/documentation/contacts)
|
|
81
|
+
- [SQLite3 gem](https://github.com/sparklemotion/sqlite3-ruby)
|
|
82
|
+
- macOS `AddressBook.framework` private headers (reverse-engineered)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
Stan Carver II
|
|
86
|
+
Made in Texas 🤠
|
|
87
|
+
https://stancarver.com
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# examples/deduplicate_contacts.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Find duplicate contacts by email address
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/deduplicate_contacts.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
require 'abbu/utils/deduplicator'
|
|
11
|
+
|
|
12
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
13
|
+
|
|
14
|
+
dupes = Abbu::Utils::Deduplicator.new(archive.contacts).duplicates
|
|
15
|
+
|
|
16
|
+
if dupes.empty?
|
|
17
|
+
puts 'No duplicates found.'
|
|
18
|
+
else
|
|
19
|
+
puts "Found #{dupes.size} duplicate email(s):\n\n"
|
|
20
|
+
dupes.each do |email, contacts|
|
|
21
|
+
puts "Duplicate: #{email}"
|
|
22
|
+
contacts.each { |c| puts " - #{c.full_name}" }
|
|
23
|
+
puts
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# examples/export_to_api.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Sync contacts from a .abbu archive to a JSON API (CRM / Rodeo pattern)
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/export_to_api.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
require 'net/http'
|
|
11
|
+
require 'json'
|
|
12
|
+
|
|
13
|
+
API_ENDPOINT = 'https://api.example.com/contacts'
|
|
14
|
+
|
|
15
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
16
|
+
|
|
17
|
+
archive.contacts.each do |c|
|
|
18
|
+
uri = URI(API_ENDPOINT)
|
|
19
|
+
|
|
20
|
+
response = Net::HTTP.post(
|
|
21
|
+
uri,
|
|
22
|
+
{ name: c.full_name, email: c.emails.first, phone: c.phones.first }.to_json,
|
|
23
|
+
'Content-Type' => 'application/json'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
puts "#{c.full_name} → #{response.code}"
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# examples/export_to_csv.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Export all contacts from a .abbu archive to contacts.csv
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/export_to_csv.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
require 'csv'
|
|
11
|
+
|
|
12
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
13
|
+
|
|
14
|
+
CSV.open('contacts.csv', 'w') do |csv|
|
|
15
|
+
csv << %w[Name Email Phone Company]
|
|
16
|
+
|
|
17
|
+
archive.contacts.each do |c|
|
|
18
|
+
csv << [c.full_name, c.emails.first, c.phones.first, c.company]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
puts "Exported #{archive.contacts.count} contacts to contacts.csv"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# examples/export_to_json.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Export all contacts from a .abbu archive to contacts.json
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/export_to_json.rb Contacts.abbu
|
|
8
|
+
# bundle exec ruby examples/export_to_json.rb Contacts.abbu | jq .
|
|
9
|
+
|
|
10
|
+
require 'abbu'
|
|
11
|
+
|
|
12
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
13
|
+
|
|
14
|
+
Abbu::Exporters::JsonExporter
|
|
15
|
+
.new(archive.contacts)
|
|
16
|
+
.to_file('contacts.json')
|
|
17
|
+
|
|
18
|
+
puts "Exported #{archive.contacts.count} contacts to contacts.json"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# examples/export_to_vcard.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Export all contacts from a .abbu archive to contacts.vcf
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/export_to_vcard.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
|
|
11
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
12
|
+
|
|
13
|
+
Abbu::Exporters::VcardExporter
|
|
14
|
+
.new(archive.contacts)
|
|
15
|
+
.to_file('contacts.vcf')
|
|
16
|
+
|
|
17
|
+
puts "Exported #{archive.contacts.count} contacts to contacts.vcf"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# examples/stats_report.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Print a summary report of contacts in a .abbu archive
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/stats_report.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
|
|
11
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
12
|
+
contacts = archive.contacts
|
|
13
|
+
|
|
14
|
+
puts '=== Contacts Report ==='
|
|
15
|
+
puts "Total contacts : #{contacts.count}"
|
|
16
|
+
puts "With email : #{contacts.count { |c| c.emails.any? }}"
|
|
17
|
+
puts "With phone : #{contacts.count { |c| c.phones.any? }}"
|
|
18
|
+
puts "With company : #{contacts.count(&:company)}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# examples/sync_to_crm.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Stub CRM sync pattern — replace CRM.upsert with your adapter
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby examples/sync_to_crm.rb Contacts.abbu
|
|
8
|
+
|
|
9
|
+
require 'abbu'
|
|
10
|
+
|
|
11
|
+
# Replace this class with your real CRM adapter (Printavo, HubSpot, Rodeo, etc.)
|
|
12
|
+
class CRM
|
|
13
|
+
def self.upsert(contact)
|
|
14
|
+
puts "Syncing #{contact.full_name}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
archive = Abbu.open(ARGV[0] || 'Contacts.abbu')
|
|
19
|
+
|
|
20
|
+
archive.contacts.each do |c|
|
|
21
|
+
CRM.upsert(c)
|
|
22
|
+
end
|
data/lib/abbu/archive.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# lib/abbu/archive.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'parsers/sqlite_parser'
|
|
5
|
+
require_relative 'parsers/plist_parser'
|
|
6
|
+
|
|
7
|
+
module Abbu
|
|
8
|
+
class Archive
|
|
9
|
+
attr_reader :path
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = Pathname.new(path)
|
|
13
|
+
validate!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def contacts
|
|
17
|
+
parser.contacts
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sqlite?
|
|
21
|
+
db_paths.any?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def validate!
|
|
27
|
+
raise ArgumentError, "ABBU path not found: #{@path}" unless @path.exist?
|
|
28
|
+
raise ArgumentError, "Not a directory bundle: #{@path}" unless @path.directory?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def db_paths
|
|
32
|
+
@db_paths ||= @path.glob('**/*.abcddb')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def records_path
|
|
36
|
+
@records_path ||= @path.join('Records')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parser
|
|
40
|
+
if sqlite?
|
|
41
|
+
Parsers::SqliteParser.new(db_paths)
|
|
42
|
+
else
|
|
43
|
+
Parsers::PlistParser.new(records_path)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/abbu/contact.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# lib/abbu/contact.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Abbu
|
|
5
|
+
class Contact
|
|
6
|
+
attr_accessor :first_name, :last_name, :emails, :phones, :company
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@emails = []
|
|
10
|
+
@phones = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def full_name
|
|
14
|
+
[first_name, last_name].compact.join(' ')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#<Abbu::Contact first_name=#{first_name.inspect} last_name=#{last_name.inspect} " \
|
|
19
|
+
"emails=#{emails.inspect} phones=#{phones.inspect}>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def inspect
|
|
23
|
+
to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# lib/abbu/exporters/csv_exporter.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'csv'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Exporters
|
|
8
|
+
class CsvExporter
|
|
9
|
+
def initialize(contacts)
|
|
10
|
+
@contacts = contacts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_file(path)
|
|
14
|
+
CSV.open(path, 'w') do |csv|
|
|
15
|
+
csv << headers
|
|
16
|
+
@contacts.each { |c| csv << row(c) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_stdout
|
|
21
|
+
puts(CSV.generate do |csv|
|
|
22
|
+
csv << headers
|
|
23
|
+
@contacts.each { |c| csv << row(c) }
|
|
24
|
+
end)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def headers
|
|
30
|
+
%w[Name Email Phone Company]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def row(contact)
|
|
34
|
+
[contact.full_name, contact.emails.first, contact.phones.first, contact.company]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# lib/abbu/exporters/json_exporter.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
module Exporters
|
|
8
|
+
class JsonExporter
|
|
9
|
+
def initialize(contacts)
|
|
10
|
+
@contacts = contacts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_file(path)
|
|
14
|
+
File.write(path, JSON.pretty_generate(payload))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_stdout
|
|
18
|
+
puts JSON.pretty_generate(payload)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def payload
|
|
24
|
+
@contacts.map do |c|
|
|
25
|
+
{
|
|
26
|
+
name: c.full_name,
|
|
27
|
+
emails: c.emails,
|
|
28
|
+
phones: c.phones,
|
|
29
|
+
company: c.company
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# lib/abbu/exporters/vcard_exporter.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Abbu
|
|
5
|
+
module Exporters
|
|
6
|
+
class VcardExporter
|
|
7
|
+
def initialize(contacts)
|
|
8
|
+
@contacts = contacts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_file(path)
|
|
12
|
+
File.write(path, generate)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_stdout
|
|
16
|
+
puts generate
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def generate
|
|
22
|
+
@contacts.map { |c| vcard_for(c) }.join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def vcard_for(contact)
|
|
26
|
+
lines = ['BEGIN:VCARD', 'VERSION:3.0']
|
|
27
|
+
lines << "FN:#{contact.full_name}"
|
|
28
|
+
lines << "N:#{contact.last_name};#{contact.first_name};;;"
|
|
29
|
+
lines << "ORG:#{contact.company}" if contact.company
|
|
30
|
+
contact.emails.each { |e| lines << "EMAIL:#{e}" }
|
|
31
|
+
contact.phones.each { |p| lines << "TEL:#{p}" }
|
|
32
|
+
lines << 'END:VCARD'
|
|
33
|
+
lines.join("\n")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# lib/abbu/parsers/plist_parser.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Abbu
|
|
5
|
+
module Parsers
|
|
6
|
+
class PlistParser
|
|
7
|
+
def initialize(path)
|
|
8
|
+
@path = path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def contacts
|
|
12
|
+
warn 'Plist parsing not yet implemented — no .abcddb found in this archive.'
|
|
13
|
+
[]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# lib/abbu/parsers/sqlite_parser.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sqlite3'
|
|
5
|
+
require_relative '../contact'
|
|
6
|
+
|
|
7
|
+
module Abbu
|
|
8
|
+
module Parsers
|
|
9
|
+
class SqliteParser
|
|
10
|
+
def initialize(db_paths)
|
|
11
|
+
@db_paths = Array(db_paths)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def contacts
|
|
15
|
+
@db_paths.flat_map do |db_path|
|
|
16
|
+
parse_db(db_path)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def parse_db(db_path)
|
|
23
|
+
db = SQLite3::Database.new(db_path.to_s)
|
|
24
|
+
db.results_as_hash = true
|
|
25
|
+
records(db).map { |row| build_contact(db, row) }
|
|
26
|
+
ensure
|
|
27
|
+
db&.close
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def records(db)
|
|
31
|
+
db.execute('SELECT * FROM ZABCDRECORD')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emails_for(db, record_id)
|
|
35
|
+
db.execute(
|
|
36
|
+
'SELECT ZADDRESSNORMALIZED FROM ZABCDEMAILADDRESS WHERE ZOWNER = ?',
|
|
37
|
+
record_id
|
|
38
|
+
).filter_map { |row| row['ZADDRESSNORMALIZED'] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def phones_for(db, record_id)
|
|
42
|
+
db.execute(
|
|
43
|
+
'SELECT ZFULLNUMBER FROM ZABCDPHONENUMBER WHERE ZOWNER = ?',
|
|
44
|
+
record_id
|
|
45
|
+
).filter_map { |row| row['ZFULLNUMBER'] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_contact(db, row)
|
|
49
|
+
contact = Contact.new
|
|
50
|
+
contact.first_name = row['ZFIRSTNAME']
|
|
51
|
+
contact.last_name = row['ZLASTNAME']
|
|
52
|
+
contact.company = row['ZORGANIZATION']
|
|
53
|
+
contact.emails = emails_for(db, row['Z_PK'])
|
|
54
|
+
contact.phones = phones_for(db, row['Z_PK'])
|
|
55
|
+
contact
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# lib/abbu/utils/deduplicator.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Abbu
|
|
5
|
+
module Utils
|
|
6
|
+
class Deduplicator
|
|
7
|
+
def initialize(contacts)
|
|
8
|
+
@contacts = contacts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def duplicates
|
|
12
|
+
@contacts
|
|
13
|
+
.group_by { |c| c.emails.first }
|
|
14
|
+
.reject { |k, _| k.nil? }
|
|
15
|
+
.select { |_, v| v.size > 1 }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/abbu/version.rb
ADDED
data/lib/abbu.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# lib/abbu.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'abbu/version'
|
|
5
|
+
require_relative 'abbu/contact'
|
|
6
|
+
require_relative 'abbu/archive'
|
|
7
|
+
require_relative 'abbu/parsers/sqlite_parser'
|
|
8
|
+
require_relative 'abbu/parsers/plist_parser'
|
|
9
|
+
require_relative 'abbu/exporters/csv_exporter'
|
|
10
|
+
require_relative 'abbu/exporters/json_exporter'
|
|
11
|
+
require_relative 'abbu/exporters/vcard_exporter'
|
|
12
|
+
require_relative 'abbu/utils/deduplicator'
|
|
13
|
+
|
|
14
|
+
module Abbu
|
|
15
|
+
def self.open(path)
|
|
16
|
+
Archive.new(path)
|
|
17
|
+
end
|
|
18
|
+
end
|
data/tasks/abbu.rake
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# tasks/abbu.rake
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'abbu'
|
|
5
|
+
require 'csv'
|
|
6
|
+
|
|
7
|
+
namespace :abbu do
|
|
8
|
+
desc 'Export contacts to CSV — rake abbu:export[file=Contacts.abbu]'
|
|
9
|
+
task :export, [:file] do |_, args|
|
|
10
|
+
archive = Abbu.open(args[:file] || 'Contacts.abbu')
|
|
11
|
+
|
|
12
|
+
CSV.open('contacts.csv', 'w') do |csv|
|
|
13
|
+
csv << %w[Name Email Phone Company]
|
|
14
|
+
archive.contacts.each do |c|
|
|
15
|
+
csv << [c.full_name, c.emails.first, c.phones.first, c.company]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts "Exported #{archive.contacts.count} contacts to contacts.csv"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'Find duplicate contacts — rake abbu:dedupe[file=Contacts.abbu]'
|
|
23
|
+
task :dedupe, [:file] do |_, args|
|
|
24
|
+
require 'abbu/utils/deduplicator'
|
|
25
|
+
|
|
26
|
+
archive = Abbu.open(args[:file] || 'Contacts.abbu')
|
|
27
|
+
dupes = Abbu::Utils::Deduplicator.new(archive.contacts).duplicates
|
|
28
|
+
|
|
29
|
+
if dupes.empty?
|
|
30
|
+
puts 'No duplicates found.'
|
|
31
|
+
else
|
|
32
|
+
dupes.each do |email, contacts|
|
|
33
|
+
puts "Duplicate: #{email}"
|
|
34
|
+
contacts.each { |c| puts " - #{c.full_name}" }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc 'Print contact stats — rake abbu:stats[file=Contacts.abbu]'
|
|
40
|
+
task :stats, [:file] do |_, args|
|
|
41
|
+
archive = Abbu.open(args[:file] || 'Contacts.abbu')
|
|
42
|
+
contacts = archive.contacts
|
|
43
|
+
|
|
44
|
+
puts "Total contacts : #{contacts.count}"
|
|
45
|
+
puts "With email : #{contacts.count { |c| c.emails.any? }}"
|
|
46
|
+
puts "With phone : #{contacts.count { |c| c.phones.any? }}"
|
|
47
|
+
puts "With company : #{contacts.count(&:company)}"
|
|
48
|
+
end
|
|
49
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: abbu
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stan Carver II
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: sqlite3
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: csv
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: guard
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.18'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.18'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: guard-rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '4.7'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '4.7'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: guard-rubocop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.5'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.5'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rake
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '13.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '13.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.13'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.13'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '1.65'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '1.65'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: rubocop-performance
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '1.21'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '1.21'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: rubocop-rake
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0.6'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0.6'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: simplecov
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - "~>"
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0.22'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0.22'
|
|
167
|
+
description: Parse Apple Address Book Archive (.abbu) files and export contacts to
|
|
168
|
+
CSV, JSON, or vCard. Supports modern SQLite-backed archives and legacy plist-based
|
|
169
|
+
records.
|
|
170
|
+
email:
|
|
171
|
+
- stan@a1webconsulting.com
|
|
172
|
+
executables:
|
|
173
|
+
- abbu
|
|
174
|
+
extensions: []
|
|
175
|
+
extra_rdoc_files: []
|
|
176
|
+
files:
|
|
177
|
+
- CHANGELOG.md
|
|
178
|
+
- LICENSE
|
|
179
|
+
- README.md
|
|
180
|
+
- bin/abbu
|
|
181
|
+
- bin/cleanse
|
|
182
|
+
- bin/console
|
|
183
|
+
- bin/dev
|
|
184
|
+
- bin/lint
|
|
185
|
+
- bin/outdated
|
|
186
|
+
- bin/test
|
|
187
|
+
- docs/ABBU.md
|
|
188
|
+
- examples/deduplicate_contacts.rb
|
|
189
|
+
- examples/export_to_api.rb
|
|
190
|
+
- examples/export_to_csv.rb
|
|
191
|
+
- examples/export_to_json.rb
|
|
192
|
+
- examples/export_to_vcard.rb
|
|
193
|
+
- examples/stats_report.rb
|
|
194
|
+
- examples/sync_to_crm.rb
|
|
195
|
+
- lib/abbu.rb
|
|
196
|
+
- lib/abbu/archive.rb
|
|
197
|
+
- lib/abbu/contact.rb
|
|
198
|
+
- lib/abbu/exporters/csv_exporter.rb
|
|
199
|
+
- lib/abbu/exporters/json_exporter.rb
|
|
200
|
+
- lib/abbu/exporters/vcard_exporter.rb
|
|
201
|
+
- lib/abbu/parsers/plist_parser.rb
|
|
202
|
+
- lib/abbu/parsers/sqlite_parser.rb
|
|
203
|
+
- lib/abbu/utils/deduplicator.rb
|
|
204
|
+
- lib/abbu/version.rb
|
|
205
|
+
- tasks/abbu.rake
|
|
206
|
+
homepage: https://github.com/scarver2/abbu
|
|
207
|
+
licenses:
|
|
208
|
+
- MIT
|
|
209
|
+
metadata:
|
|
210
|
+
allowed_push_host: https://rubygems.org
|
|
211
|
+
homepage_uri: https://github.com/scarver2/abbu
|
|
212
|
+
source_code_uri: https://github.com/scarver2/abbu
|
|
213
|
+
changelog_uri: https://github.com/scarver2/abbu/blob/master/CHANGELOG.md
|
|
214
|
+
rubygems_mfa_required: 'true'
|
|
215
|
+
post_install_message:
|
|
216
|
+
rdoc_options: []
|
|
217
|
+
require_paths:
|
|
218
|
+
- lib
|
|
219
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
220
|
+
requirements:
|
|
221
|
+
- - ">="
|
|
222
|
+
- !ruby/object:Gem::Version
|
|
223
|
+
version: '3.2'
|
|
224
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
225
|
+
requirements:
|
|
226
|
+
- - ">="
|
|
227
|
+
- !ruby/object:Gem::Version
|
|
228
|
+
version: '0'
|
|
229
|
+
requirements: []
|
|
230
|
+
rubygems_version: 3.4.19
|
|
231
|
+
signing_key:
|
|
232
|
+
specification_version: 4
|
|
233
|
+
summary: Read and process Apple Contacts .abbu archives in Ruby.
|
|
234
|
+
test_files: []
|