Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,85 @@
# 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).

## Development

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.
19 changes: 19 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
/(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\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.
20 changes: 3 additions & 17 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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.
28 changes: 28 additions & 0 deletions src/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = {
Expand All @@ -182,5 +209,6 @@ if (typeof module !== 'undefined' && module.exports) {
findMatchGroup,
getLastMessageDate,
main,
dateFormatterFactory,
};
}
59 changes: 15 additions & 44 deletions src/examples.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -32,19 +34,9 @@ function removeBrookstoneAffiliateInfo() {
const datePattern =
// eslint-disable-next-line no-useless-escape
/Brookstone:[\w\s\d!$%\/-]*?(?<exp>(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 = /(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\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(
/(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\d+)/
);
main(query, datePattern, { isDryRun: false, dateFormatter });
}

Expand Down Expand Up @@ -92,15 +84,10 @@ function removeIATeamInfo() {
function removeMoneyHeroInfo() {
const query = 'from:(MoneyHero <noreply@promo.moneyhero.com.hk>) is:trash';
const datePattern = /\d{4}年\d{1,2}月\d{1,2}日或之前/gm;
const dateFormatter = (textWithDate, lastMessageDate) => {
const re =
/(?<year>\d{4})年(?<month1>\d{1,2})月(?<enddate>\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(
/(?<year>\d{4})年(?<month1>\d{1,2})月(?<enddate>\d{1,2})日或之前/,
false
);
main(query, datePattern, { isDryRun: false, dateFormatter });
}

Expand All @@ -114,15 +101,9 @@ function removeNamecheapAffiliateInfo() {
const query = 'from:(Namecheap Affiliate Team) -label:affiliate-program';
const datePattern =
/(?<exp>(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 = /(?<month1>\w+)\s(\d+-)?s?(?<enddate>\d+)/;
const matches = re.exec(textWithDate);
if (!matches) {
return null;
}
return `${year}-${matches.groups.month1}.${matches.groups.enddate}`;
};
const dateFormatter = dateFormatterFactory(
/(?<month1>\w+)\s(\d+-)?s?(?<enddate>\d+)/
);
main(query, datePattern, { isDryRun: false, mode: 'html', dateFormatter });
}

Expand All @@ -146,18 +127,8 @@ function removeWondershareAffiliateInfo() {
function removeYandexWebmasterInfo() {
const query = 'from:(Yandex.Webmaster <devnull@webmaster.yandex.ru>)';
const datePattern = /for the week of (?<exp>\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} ?(?<month1>\w*) \W ?(?<enddate>\d{1,2}) (?<month2>\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} ?(?<month1>\w*) \W ?(?<enddate>\d{1,2}) (?<month2>\w+)/
);
main(query, datePattern, { isDryRun: false, dateFormatter });
}
58 changes: 57 additions & 1 deletion test/code.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { checkOverdue, findMessages } = require('../src/code.js');
const { checkOverdue, findMessages, dateFormatterFactory } = require('../src/code.js');

describe('Core Functions', () => {

Expand Down Expand Up @@ -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 = /(?<month1>\w{3})\s(?<enddate>\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 = /(?<year>\d{4})-(?<month1>\w{3})-(?<enddate>\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 =
/(?<month1>\w{3})\s\d{1,2}\s-\s(?<month2>\w{3})\s(?<enddate>\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 = /(?<month1>\w{3})/;
const formatter = dateFormatterFactory(pattern);
const lastMessageDate = new Date('2023-01-01');
const result = formatter('Jan', lastMessageDate);
expect(result).toBeNull();
});
});