{"id":19862829,"url":"https://github.com/adiwajshing/queenfisher","last_synced_at":"2025-05-02T04:30:54.963Z","repository":{"id":55057734,"uuid":"264442220","full_name":"adiwajshing/Queenfisher","owner":"adiwajshing","description":"Cross-platform Google APIs for Swift built on Codable \u0026 NIO","archived":false,"fork":false,"pushed_at":"2023-03-23T17:42:56.000Z","size":832,"stargazers_count":25,"open_issues_count":3,"forks_count":9,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-06T22:37:53.213Z","etag":null,"topics":["email","email-read","email-receiver","email-sending","gmail","gmail-api","gmail-api-wrapper","google","google-api","google-sheets","nio","sheets-api","swift-library","swift-nio","swift-package-manager"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adiwajshing.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-16T13:22:37.000Z","updated_at":"2024-07-31T04:40:02.000Z","dependencies_parsed_at":"2022-08-14T10:30:41.612Z","dependency_job_id":null,"html_url":"https://github.com/adiwajshing/Queenfisher","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adiwajshing%2FQueenfisher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adiwajshing%2FQueenfisher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adiwajshing%2FQueenfisher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adiwajshing%2FQueenfisher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adiwajshing","download_url":"https://codeload.github.com/adiwajshing/Queenfisher/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251986738,"owners_count":21675950,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","email-read","email-receiver","email-sending","gmail","gmail-api","gmail-api-wrapper","google","google-api","google-sheets","nio","sheets-api","swift-library","swift-nio","swift-package-manager"],"created_at":"2024-11-12T15:12:48.122Z","updated_at":"2025-05-02T04:30:53.744Z","avatar_url":"https://github.com/adiwajshing.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Queenfisher - Cross-Platform Google APIs for Swift built with NIO\n\n## What's Done:\n\n- [x] Authenticating using OAuth \u0026 using refresh tokens to continually fetch new access tokens\n- [x] Authenticating using a service account\n- [x] **GMail** -- reading, modifying, fetching, sending \u0026 replying to emails\n- [x] **Spreadsheets** -- reading, modifying \u0026 writing to sheets \n- [x] Synchronize \u0026 maintain a database on Sheets\n\n## Installing\n\n1. Queenfisher is written in Swift 5.2, so you need either **XCode 11.4** or **Swift 5.2** installed on your system.\n2. Add Queenfisher to your swift package: \n``` swift\n\t...\n\tdependencies: [\n\t\t// Dependencies declare other packages that this package depends on.\n\t\t.package(url: \"https://github.com/adiwajshing/Queenfisher.git\", from: \"0.1.0\")\n\t],\n\ttargets: [\n\t\t.target(name: \"MyTarget\", dependencies: [\"Queenfisher\", ...])\n\t]\n\t...\n```\n3. Finally, import **Queenfisher** in your code using:\n``` swift\n\timport Queenfisher\n```\n\n## Authenticating with Google\n\n1. Before you can use these APIs, you need to have a project setup on Google Cloud Platform, you can create one [here](https://console.developers.google.com/projectcreate). \n2. Once you have a project setup, you must enable the APIs you want to use. **Queenfisher** currently wraps around the [GMail](https://developers.google.com/gmail/api/v1/reference) \u0026 [Sheets](https://developers.google.com/sheets/api/reference/rest) API, so you can enable either or both.\n3. To authenticate using [O-Auth](https://developers.google.com/identity/protocols/oauth2/web-server)\n\t- Create \u0026 download your client secret, learn how to do that [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred).\n\t- Store the downloaded JSON somewhere nice \u0026 safe.\n\t- Now you can load the JSON \u0026 generate an access token:\n\t``` swift\n\timport Queenfisher\n\timport NIO\n\t\n\tlet pathToSecret = URL(fileURLWithPath: \"Path/to/client_secret.json\")\n\tlet pathToToken = URL(fileURLWithPath: \"Path/to/my_token.json\") // place to save the generated token\n\t\n\tlet client: GoogleOAuthClient = try .loading(from: pathToSecret)\n\t// generate the authentication url where you can sign in \u0026 get your access token\n\tlet authUrl = client.authUrl(for: .mailAll + .sheets) // authenticate for full access to mail \u0026 spreadsheets\n\tprint (\"sign in here \u0026 paste the code from the link below: \\(authUrl)\") // open the url in a browser\n\t/*\n\t\tOnce you sign off on the permissions, google will redirect you to the url you specified in the client secret\n\t\tIf you don't have a server listening, you can just extract the code \u0026 paste it here, and you will get your access \u0026 refresh tokens\n\t\tThe code will be in the url query like: http://localhost:8080?code=abcdefg\u0026scope=blahblah\n\t\tPaste `abcdefg` below\n\t*/\n\tlet code = readLine(strippingNewline: true)!\n\tlet accessToken = try client.fetchToken(fromCode: code).wait() // will exchange code for access \u0026 refresh tokens\n\tprint(\"got access token: \\($0)\")\n\t\n\t/* You can now use this access token for sheets or gmail */\n\t\n\ttry JSONEncoder().encode(accessToken).write(to: pathToToken) // save the token as a JSON\n\t```\n\t- To continually ensure you have an active token, you can [create a factory](https://developers.google.com/identity/protocols/oauth2/web-server#offline). New tokens are fetched using the refresh token whenever one expires. Do note, that refresh tokens never expire, they stop working whenever the user revokes access to your GCP project.\n\t``` swift\n\t// get your client secret\n\tlet client: GoogleOAuthClient = try .loading(from: pathToSecret)\n\t// create an authentication factory using the access token \u0026 secret\n\tlet authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken))\n\t/*\n\tUse authFactory as your access mediator when accessing APIs. \n\tThis will ensure you always have an active access token\n\t*/\n\t```\n4. To authenticate using a [Service Account](https://developers.google.com/identity/protocols/oauth2/service-account):\n\t- Create a service account or use one you already have, learn about creating one [here](https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount).\n\t- Download the credentials of said service account.\n\t``` swift\n\timport Queenfisher\n\t\n\tlet pathToAcc = URL(fileURLWithPath: \"Path/to/service_account.json\")\n\tlet serviceAcc: GoogleServiceAccount = try .loading(fromJSONAt: pathToAcc)\n\t\n\tlet authFactory = serviceAcc.factory (forScope: .sheets) // get authentication for sheets\n\t/*\n\tUse authFactory as your access mediator when accessing APIs. \n\tThis will ensure you always have an active access token\n\t*/\n\t```\n\t\n## GMail API\n\n- Create an instance\n\t``` swift\n\timport Queenfisher\n\timport Promises\n\t\n\t// create an authentication factory using the access token \u0026 secret\n\t// make sure your token has access to GMail\n\t// do note, service accounts cannot access GMail unless with GSuite accounts\n\tlet client: GoogleOAuthClient = try .loading(from: pathToSecret)\n\tlet authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken))\n\t\n\tlet gmail: GMail = .init(using: authFactory)\n\t\n\tlet profile = try gmail.profile().wait()\n\tprint (\"Oh hello: \\(profile.emailAddress)\") // print email address\n\t```\n- [Listing emails](https://developers.google.com/gmail/api/v1/reference/users/messages/list)\n\t``` swift\n\tgmail.list() // lists all messages in inbox, sent \u0026 drafts ordered by timestamp\n\t.map {\n\t\tprint (\"got \\($0.resultSizeEstimate) messages\")\n\t\tif let messages = $0.messages {\n\t\t\tfor m in messages { // metadata of messages\n\t\t\t\tprint (\"id: \\(m.id)\")\n\t\t\t}\n\t\t}\n\t}\n\t```\n\tYou can refine your search by specifying query parameters mentioned [here](https://support.google.com/mail/answer/7190?hl=en). For example:\n\t``` swift\n\tgmail.list(q: \"is:unread\") // lists all unread messages\n\tgmail.list(q: \"subject:permission\") // subject contains the word `permission`\n\tgmail.list(q: \"from:xyz@yahoo.com\") // all emails from this email address\n\t```\n- [Reading emails](https://developers.google.com/gmail/api/v1/reference/users/messages/get)\n\t``` swift\n\tgmail.list() // lists all messages in inbox, sent \u0026 drafts ordered by timestamp\n\t.flatMap { gmail.get(id: $0.messages![0].id, format: .full) } // get the first email received\n\t.map { \n\t\tprint (\"email from: \\($0.from!)\") \n\t\tprint (\"email subject: \\($0.subject!)\") \n\t\tprint (\"email snippet: \\($0.snippet!)\") \n\t}\n\t```\n\tDive deeper into the [GMail.Message](Sources/Queenfisher/GMail/GMail.Message.swift) class to get the attachements \u0026 the entire text of the email.\n- [Sending emails](https://developers.google.com/gmail/api/v1/reference/users/messages/send)\n\t``` swift\n\tlet attachFile = URL(fileURLWithPath: \"Path/to/fave_image.jpeg\")\n\t\n\tlet mail: GMail.Message = .init(to: [ .namedEmail(\"Myself \u0026 I\", profile.emailAddress) ],\n\t\t\t\t\t\t\t\t\tsubject: \"Hello\",\n\t\t\t\t\t\t\t\t\ttext: \"My name \u003cb\u003eJeff\u003c/b\u003e.\",\n\t\t\t\t\t\t\t\t\tattachments: [ try! .attachment(fileAt: attachFile) ])\n\t\n\tgmail.send (message: mail)\n\t.whenComplete { print (\"yay sent mail with ID: \\($0.id)\") }\n\t.whenFailure { print (\"error in sending: \\($0)\") }\n\t```\n\tThe `text` in emails must be some html text.\n- [Replying to emails](https://developers.google.com/gmail/api/v1/reference/users/messages/send)\n\t``` swift\n\tlet profile = try await(gmail.profile()) // get profile\n\tgmail.list()\n\t.flatMap { gmail.get(id: $0.messages![0].id, format: .full) } // get the first email received\n\t.flatMap { message -\u003e EventLoopFuture\u003cGMail.Message\u003e in\n\t\tlet isMailFromMe = $0.from!.email == profile.emailAddress // determine if the email was sent by me\n\t\tlet reply: GMail.Message = GMail.Message(replyingTo: message, \n\t\t\t\t\t\t\t\t\t\t\t\tfromMe: isMailFromMe, \n\t\t\t\t\t\t\t\t\t\t\t\ttext: \"Wow this is a reply\")!\n\t\treturn gmail.send (message: reply)\n\t}\n\t.whenComplete { print (\"yay sent reply with ID: \\($0.id)\") }\n\t```\n- Fetching Emails\n\t``` swift\n\t// fetch unread emails every 60 seconds\n\t// note: once a mail is forwarded to this handler, it will not be forwarded again in the future\n\tgmail.fetch(over: .seconds(60), q: \"is:unread\") { result in\n\t\tswitch result {\n\t\tcase .success(let messages):\n\t\t\tprint (\"got \\(messages.count) new messages\")\n\t\t\tbreak\n\t\tcase .failure(let error):\n\t\t\tprint(\"Oh no, got an error: \\(error)\")\n\t\t\tbreak\n\t\t}\n\t}\n\t```\n- Misc Tasks\n\t* [Marking emails as read](https://developers.google.com/gmail/api/v1/reference/users/messages/modify)\n\t``` swift\n\t\tgmail.markRead (id: idOfTheMessage)\n\t\t.whenComplete { print (\"yay read mail with ID: \\($0.id)\") }\n\t```\n\t* [Trashing emails](https://developers.google.com/gmail/api/v1/reference/users/messages/trash)\n\t``` swift\n\t\tgmail.trash (id: idOfTheMessage)\n\t\t.whenComplete { print (\"yay trashed mail with ID: \\($0.id)\") }\n\t```\n\t* [Modifying labels on emails](https://developers.google.com/gmail/api/v1/reference/users/messages/modify)\n\t``` swift\n\t\tgmail.modify (id: idOfTheMessage, adddingLabelIds: [\"UNREAD\"]) // effectively mark an email as unread\n\t\t.whenComplete { print (\"yay modified mail with ID: \\($0.id)\") }\n\t```\n\n## Sheets API\n\n- [Getting](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get) a Spreadsheet:\n\t``` swift\n\timport Queenfisher\n\timport NIO\n\n\t// create an authentication factory using the access token \u0026 secret\n\t// make sure your token has access to GMail\n\t// do note, service accounts cannot access GMail unless with GSuite accounts\n\tlet client: GoogleOAuthClient = try .loading(from: pathToSecret)\n\tlet authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken))\n\t\n\tlet spreadsheetId = \"abcdefghi\" // insert actual spreadsheet ID\n\t\n\tlet spreadsheet: Spreadsheet = try .get(spreadsheetId, using: authFactory).wait ()\n\tprint(\"Got spreadsheet '\\(spreadsheet.properties.title)', sheets: \\(spreadsheet.sheets.map({$0.properties.title}))\") \n\t```\n- [Writing](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#updatecellsrequest) rows to a spreadsheet:\n\t``` swift\n\t// get the sheet ID, it's the unique ID for every sheet, you'll need it for almost all operations\n\tlet sheetId = spreadsheet.sheet (forTitle: \"Sheet 1\")!.properties.sheetId!\n\t\n\tlet rows = [\n\t\t[\"hello\", \"this\", \"is\", \"jeff\"],\n\t\t[\"yes\", \"my\", \"name\", \"jeff\"],\n\t\t[\"of course\", \"this\", \"is\", \"jeff\"]\n\t]\n\t// write these rows to the start of the spreadsheet\n\tspreadsheet.writeRows (sheetId: sheetId, rows: rows, starting: .cell(0,0))\n\t.whenComplete { _ in print (\"yay done\") }\n\t```\n- [Appending](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#appendcellsrequest) rows to a spreadsheet:\n\t``` swift\n\t// get the sheet ID, it's the unique ID for every sheet, you'll need it for almost all operations\n\tlet sheetId = spreadsheet.sheet (forTitle: \"Sheet 1\")!.properties.sheetId!\n\t\n\tlet rows = [\n\t\t[\"wow\", \"more\", \"rows\", \"!\"],\n\t\t[\"yes\", \"this\", \"is\", \"great\"]\n\t]\n\t// append these rows after the last row with data in the sheet\n\tspreadsheet.appendRows (sheetId: sheetId, rows: rows)\n\t.whenComplete { _ in print (\"yay done\") }\n\t```\n- [Reading](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get) from a spreadsheet:\n\t``` swift\n\tlet sheetId = spreadsheet.sheet (forTitle: \"Sheet 1\")!.properties.sheetId!\n\t\n\tspreadsheet.read (sheetId: sheetId)\n\t.whenComplete { print (\"\\($0.values)\") }\n\t\n\t/* or if you want to read a specific range */\n\tspreadsheet.read (sheetId: sheetId, range: (.row(1), .row(5))) // read all columns in row index 1 to 5\n\t.whenComplete { print (\"\\($0.values)\") }\n\t```\n- [Inserting](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#insertdimensionrequest) empty rows/columns into a sheet:\n\t``` swift\n\tspreadsheet.insert(sheetId: sheetId, range: 2..\u003c4, dimension: .columns) // insert 2 columns at index 2\n\t.whenComplete { _ in print (\"yay inserted\") }\n\t```\n- [Appending](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#appenddimensionrequest) empty rows/columns into a sheet:\n\t``` swift\n\tspreadsheet.append(sheetId: sheetId, size: 3, dimension: .columns) // append 3 columns\n\t.whenComplete { _ in print (\"yay appended\") }\n\t```\n- [Moving](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#movedimensionrequest) rows/columns in a sheet:\n\t``` swift\n\tspreadsheet.move(sheetId: sheetId, range: 2..\u003c3, to: 2, to: 1, dimension: .rows) // move rows 2-3 to index 1\n\t.whenComplete { _ in print (\"yay moved\") }\n\t```\n- [Deleting](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#deletedimensionrequest) rows/columns in a sheet:\n\t``` swift\n\tspreadsheet.delete(sheetId: sheetId, range: 2..\u003c3, to: 2, dimension: .rows) // deletes rows at indexes 2-3\n\t.whenComplete { _ in print (\"yay deleted\") }\n\t```\n- [Adding](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#addsheetrequest) rows/columns in a sheet:\n\t``` swift\n\tspreadsheet.create(title: \"Name of the sheet\", dimensions: .init(rowCount: 10, columnCount: 5))\n\t.whenComplete { print (\"yay created with ID: \\($0.replies.first!.addSheet!.properties!.sheetId)\") }\n\t```\n- [Deleting](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#deletesheetrequest) a sheet from a spreadsheet:\n\t``` swift\n\tspreadsheet.delete(sheetId: sheetId)\n\t.whenComplete { _ in print (\"yay deleted\") }\n\t```\n- [Clearing](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#updatecellsrequest) a sheet:\n\t``` swift\n\tspreadsheet.clear(sheetId: sheetId) // will delete all data in the sheet\n\t.whenComplete { _ in print (\"yay cleared\") }\n\t```\n\t\nHaven't documented IndexedSheet \u0026 AtomicSheet yet :/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadiwajshing%2Fqueenfisher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadiwajshing%2Fqueenfisher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadiwajshing%2Fqueenfisher/lists"}