diff --git a/README.md b/README.md index 817fffc..0c864ae 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,60 @@ -# Gmail Old Email Cleaner +# Gmail Regex Cleaner [![Build Status](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/actions/workflows/ci.yml/badge.svg)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/actions/workflows/ci.yml) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](/LICENSE) +[![GitHub issues](https://img.shields.io/github/issues/chriskyfung/gmail-regex-cleaner-apps-script)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/issues) +[![GitHub stars](https://img.shields.io/github/stars/chriskyfung/gmail-regex-cleaner-apps-script)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/stargazers) -This is a Google Apps Script project that helps you delete old emails in Gmail that match your custom regex filters. This can help you save space and keep your inbox organized. +A Google Apps Script that helps you delete old emails in Gmail that match your custom regex filters. This can help you save space and keep your inbox organized. -## Features +## Table of Contents -- Delete old emails in Gmail that match a regular expression -- Specify the number of days to keep the emails -- Exclude starred, important, or labeled emails from deletion -- Run the script manually or on a schedule -- Log the deleted emails and errors +* [About The Project](#about-the-project) +* [Getting Started](#getting-started) +* [Usage](#usage) +* [Development](#development) +* [Contributing](#contributing) +* [License](#license) +* [Disclaimer](#disclaimer) -## How to use +## About The Project + +This project provides a flexible way to automatically clean up your Gmail inbox by deleting old emails that match specific criteria defined by regular expressions. It's perfect for managing recurring emails like newsletters, notifications, and alerts that you don't need to keep forever. + +### Features + +* Delete old emails in Gmail that match a regular expression +* Specify the number of days to keep the emails +* Exclude starred, important, or labeled emails from deletion +* Run the script manually or on a schedule +* Log the deleted emails and errors + +## Getting Started + +> [!WARNING] +> This script will delete your emails permanently, without moving them to the trash. Please use it with caution and make sure you have a backup of your important emails. You can run the script with the `isDryRun` option set to `true` first to see what emails will be deleted. + +This section will guide you through the process of setting up and running the script. + +### Prerequisites + +* A Google account with access to Gmail and Google Drive. + +### Installation + +1. Create a new Google Apps Script project in Google Drive. +2. Copy and paste the code from `dist/code.js` and `dist/examples.js` into the script editor. +3. From the `examples.js` file, choose a function that matches your needs, or create a new one. You can then run this function from the Apps Script editor. + + For example, to run one of the pre-made functions, you would select it in the editor's function list and click **Run**. + +> [!IMPORTANT] +> When running the script for the first time, you may need to authorize it to access your Gmail account. + +4. Optionally, set up a trigger to run a function periodically. You can do this by clicking the **Triggers** icon in the left sidebar, then clicking the **Add a trigger** button, and choosing the options you want. For example, you can set the script to run every day, week, or month. + +## Usage For detailed instructions on how to set up and use this script, please see the [**Usage Guide**](./docs/usage.md). @@ -22,10 +62,24 @@ For detailed instructions on how to set up and use this script, please see the [ This project uses ESLint for linting, Prettier for formatting, Jest for testing, and Rollup for building. For more details on the development setup and build process, please see the [**Development Guide**](./docs/development.md). -## Disclaimer +## Contributing -This script is provided as is, without any warranty or liability. Use it at your own risk. Make sure to test the script before using it on your Gmail account. The script may delete emails that you want to keep, or fail to delete emails that you want to remove. The script may also exceed the quota limits of Google Apps Script or Gmail API, resulting in errors or partial execution. The author is not responsible for any loss or damage caused by the use of this script. +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +Please read the [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing. + +If you have a bug report or a feature request, please open an issue on the [GitHub Issues page](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/issues). ## License This project is distributed under the AGPL-3.0 license. You can use, modify, and distribute this project, as long as you comply with the terms and conditions in the [LICENSE](/LICENSE) file. + +## Disclaimer + +This script is provided as is, without any warranty or liability. Use it at your own risk. Make sure to test the script before using it on your Gmail account. The script may delete emails that you want to keep, or fail to delete emails that you want to remove. The script may also exceed the quota limits of Google Apps Script or Gmail API, resulting in errors or partial execution. The author is not responsible for any loss or damage caused by the use of this script. diff --git a/docs/development.md b/docs/development.md index 194d492..91722a1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -62,3 +62,22 @@ To update the timezone, run the following command **after** the build command: ```bash npm run update-timezone ``` + +### `dateFormatterFactory` + +The `dateFormatterFactory` function is a helper function that creates a `dateFormatter` function for you. It takes a regular expression pattern and an optional boolean `useLastMessageYear` as arguments. + +The pattern should contain named capture groups for `year`, `month1`, `month2` (optional), and `enddate`. The factory will then generate a function that extracts these parts from a date string and returns a formatted date string. + +Here is an example of how to use it: + +```js +const dateFormatter = dateFormatterFactory( + /(?\w{3})\s\d+-(?\w{3})?\s?(?\d+)/ +); + +// The generated dateFormatter can then be passed to the main function. +main(query, pattern, { dateFormatter }); +``` + +This is useful for creating complex date formatters without writing the same boilerplate code every time. \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md index b32255e..9fcc8f5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,12 +1,4 @@ -# How to Use - -1. Create a new Google Apps Script project in Google Drive. -2. Copy and paste the code from `dist/code.js` and `dist/examples.js` into the script editor. -3. From the `examples.js` file, choose a function that matches your needs, or create a new one. You can then run this function from the Apps Script editor. - - For example, to run one of the pre-made functions, you would select it in the editor's function list and click **Run**. - - The `main` function, which does the core work, has been updated to be more flexible. Here is how you would call it inside a custom function: +The `main` function, which does the core work, has been updated to be more flexible. Here is how you would call it inside a custom function: ```js function removeOldGoogleAlerts() { @@ -41,13 +33,7 @@ > - **`isDryRun`**: A boolean value that indicates whether to run the script in test mode or not. If `true` (the default), the script will only log the emails that match the query and the regex, but will not delete them. If `false`, the script will delete the emails permanently. It is recommended to run the script with `isDryRun` set to `true` first to make sure it works as expected. > - **`mode`**: A string that specifies whether to process the email body as plain text or HTML. Can be either `'plain'` (default) or `'html'`. If set to `'html'`, the script will strip all HTML tags from the email body before searching for the regex pattern. > - **`dateFormatter`**: A function that takes two parameters: `textWithDate` and `lastMessageDate`. The `textWithDate` is a string that contains the date part extracted from the email body. The `lastMessageDate` is a date object that represents the latest date of the email thread. The function should return a date string like `yyyy-MM-dd` that can be parsed by `new Date()`. +> > [!TIP] +> > For common date formats, you can use the `dateFormatterFactory` function to create a `dateFormatter` for you. See the [Development Guide](./development.md#dateformatterfactory) for more details. 4. Save and run the desired function from the script editor. You can use the **Run** menu or the **Run** button in the toolbar. - -> [!IMPORTANT] -> When running the script for the first time, you may need to authorize it to access your Gmail account. - -5. Optionally, set up a trigger to run a function periodically. You can do this by clicking the **Triggers** icon in the left sidebar, then clicking the **Add a trigger** button, and choosing the options you want. For example, you can set the script to run every day, week, or month. - -> [!WARNING] -> This script will delete your emails permanently, without moving them to the trash. Please use it with caution and make sure you have a backup of your important emails. You can run the script with the `isDryRun` option set to `true` first to see what emails will be deleted. diff --git a/src/code.js b/src/code.js index 6ff308b..c1338dd 100644 --- a/src/code.js +++ b/src/code.js @@ -103,6 +103,32 @@ function findMatchGroup(text, pattern) { return result === null ? null : result.groups?.exp || true; } +/** + * Creates a date formatter function from a regular expression pattern. + * @param {RegExp} pattern - The regular expression to find the date parts. + * @param {boolean} [useLastMessageYear=true] - Whether to use the last message's year. + * @returns {function(string, Date): string|null} A function that takes text and last message date, and returns a formatted date string or null. + */ +function dateFormatterFactory(pattern, useLastMessageYear = true) { + return (textWithDate, lastMessageDate) => { + const matches = pattern.exec(textWithDate); + if (!matches) { + return null; + } + const { groups } = matches; + const year = useLastMessageYear + ? Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy') + : groups.year; + const month = groups.month2 || groups.month1; + const day = groups.enddate; + + if (day && month) { + return `${year}-${month}.${day}`; + } + return null; + }; +} + /** * The main function searches for messages that match a given query string, extracts text using a * pattern, and checks if the extracted text is overdue based on a date formatter function. @@ -174,6 +200,7 @@ globalThis.findMatchGroup = findMatchGroup; globalThis.getLastMessageDate = getLastMessageDate; globalThis.findMessages = findMessages; globalThis.main = main; +globalThis.dateFormatterFactory = dateFormatterFactory; if (typeof module !== 'undefined' && module.exports) { module.exports = { @@ -182,5 +209,6 @@ if (typeof module !== 'undefined' && module.exports) { findMatchGroup, getLastMessageDate, main, + dateFormatterFactory, }; } diff --git a/src/examples.js b/src/examples.js index c838d02..ce47296 100644 --- a/src/examples.js +++ b/src/examples.js @@ -1,3 +1,5 @@ +/* global dateFormatterFactory */ + /** * The function `removeAboutMeWeeklyStats()` removes weekly stats emails from about.me with the subject * line "your weekly stats from about.me" from the trash folder. The regex pattern looks for instances @@ -32,19 +34,9 @@ function removeBrookstoneAffiliateInfo() { const datePattern = // eslint-disable-next-line no-useless-escape /Brookstone:[\w\s\d!$%\/-]*?(?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{1,2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?\s?\d{1,2})/gm; - const dateFormatter = (textWithDate, lastMessageDate) => { - const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy'); - const re = /(?\w{3})\s\d+-(?\w{3})?\s?(?\d+)/; - const matches = re.exec(textWithDate); - if (!matches) { - return null; - } - if (matches.groups.month2) { - return `${year}-${matches.groups.month2}.${matches.groups.enddate}`; - } else { - return `${year}-${matches.groups.month1}.${matches.groups.enddate}`; - } - }; + const dateFormatter = dateFormatterFactory( + /(?\w{3})\s\d+-(?\w{3})?\s?(?\d+)/ + ); main(query, datePattern, { isDryRun: false, dateFormatter }); } @@ -92,15 +84,10 @@ function removeIATeamInfo() { function removeMoneyHeroInfo() { const query = 'from:(MoneyHero ) is:trash'; const datePattern = /\d{4}年\d{1,2}月\d{1,2}日或之前/gm; - const dateFormatter = (textWithDate, lastMessageDate) => { - const re = - /(?\d{4})年(?\d{1,2})月(?\d{1,2})日或之前/; - const matches = re.exec(textWithDate); - if (!matches) { - return null; - } - return `${matches.groups.year}-${matches.groups.month1}.${matches.groups.enddate}`; - }; + const dateFormatter = dateFormatterFactory( + /(?\d{4})年(?\d{1,2})月(?\d{1,2})日或之前/, + false + ); main(query, datePattern, { isDryRun: false, dateFormatter }); } @@ -114,15 +101,9 @@ function removeNamecheapAffiliateInfo() { const query = 'from:(Namecheap Affiliate Team) -label:affiliate-program'; const datePattern = /(?(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}-\d{1,2}\.?$)/gm; - const dateFormatter = (textWithDate, lastMessageDate) => { - const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy'); - const re = /(?\w+)\s(\d+-)?s?(?\d+)/; - const matches = re.exec(textWithDate); - if (!matches) { - return null; - } - return `${year}-${matches.groups.month1}.${matches.groups.enddate}`; - }; + const dateFormatter = dateFormatterFactory( + /(?\w+)\s(\d+-)?s?(?\d+)/ + ); main(query, datePattern, { isDryRun: false, mode: 'html', dateFormatter }); } @@ -146,18 +127,8 @@ function removeWondershareAffiliateInfo() { function removeYandexWebmasterInfo() { const query = 'from:(Yandex.Webmaster )'; const datePattern = /for the week of (?\d{1,2} ?\w* \W \d{1,2} \w+)/gm; - const dateFormatter = (textWithDate, lastMessageDate) => { - const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy'); - const re = /\d{1,2} ?(?\w*) \W ?(?\d{1,2}) (?\w+)/; - const matches = re.exec(textWithDate); - if (!matches) { - return null; - } - if (matches.groups.month2) { - return `${year}-${matches.groups.month2}.${matches.groups.enddate}`; - } else { - return `${year}-${matches.groups.month1}.${matches.groups.enddate}`; - } - }; + const dateFormatter = dateFormatterFactory( + /\d{1,2} ?(?\w*) \W ?(?\d{1,2}) (?\w+)/ + ); main(query, datePattern, { isDryRun: false, dateFormatter }); } diff --git a/test/code.test.js b/test/code.test.js index a395146..600ffb5 100644 --- a/test/code.test.js +++ b/test/code.test.js @@ -1,4 +1,4 @@ -const { checkOverdue, findMessages } = require('../src/code.js'); +const { checkOverdue, findMessages, dateFormatterFactory } = require('../src/code.js'); describe('Core Functions', () => { @@ -94,3 +94,59 @@ describe('findMessages', () => { expect(result[0].plainBody).toBe('Hello'); }); }); + +describe('dateFormatterFactory', () => { + global.Utilities = { + formatDate: jest.fn((date, timeZone, format) => { + if (format === 'yyyy') { + return date.getFullYear().toString(); + } + return ''; + }), + }; + + it('should return a function', () => { + const pattern = /a/; + const formatter = dateFormatterFactory(pattern); + expect(typeof formatter).toBe('function'); + }); + + it('should return null if pattern does not match', () => { + const pattern = /a/; + const formatter = dateFormatterFactory(pattern); + const result = formatter('b', new Date()); + expect(result).toBeNull(); + }); + + it('should format date correctly using last message year', () => { + const pattern = /(?\w{3})\s(?\d{1,2})/; + const formatter = dateFormatterFactory(pattern); + const lastMessageDate = new Date('2023-01-01'); + const result = formatter('Jan 15', lastMessageDate); + expect(result).toBe('2023-Jan.15'); + }); + + it('should format date correctly using year from pattern', () => { + const pattern = /(?\d{4})-(?\w{3})-(?\d{1,2})/; + const formatter = dateFormatterFactory(pattern, false); + const result = formatter('2024-Feb-20', new Date()); + expect(result).toBe('2024-Feb.20'); + }); + + it('should use month2 if available', () => { + const pattern = + /(?\w{3})\s\d{1,2}\s-\s(?\w{3})\s(?\d{1,2})/; + const formatter = dateFormatterFactory(pattern); + const lastMessageDate = new Date('2023-01-01'); + const result = formatter('Jan 10 - Feb 20', lastMessageDate); + expect(result).toBe('2023-Feb.20'); + }); + + it('should return null if day or month is missing', () => { + const pattern = /(?\w{3})/; + const formatter = dateFormatterFactory(pattern); + const lastMessageDate = new Date('2023-01-01'); + const result = formatter('Jan', lastMessageDate); + expect(result).toBeNull(); + }); +}); \ No newline at end of file