diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2dac85424cc658bc70ca0857fe2de994e0f6a40c..d3d6230536498168004e057eb19f9f67c5f717a0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -35,7 +35,6 @@ build_branch:
     DOCKER_DRIVER: overlay2
   image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/docker:18.09
   stage: build
-  when: manual
   only:
     - merge_requests
   script:
diff --git a/docker-compose.yml b/docker-compose.yml
index 629f0d153f8bfd1da935ed0763c1da96baf344e4..789f1e441ec6e334c071915157cb819bcf4bef7f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,6 +28,9 @@ services:
       ELASTICSEARCH_NODE: ${ELASTICSEARCH_NODE}
       ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME}
       ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD}
+      MC_API_KEY: ${MC_API_KEY}
+      MC_SERVER: ${MC_SERVER}
+      MC_LIST_ID: ${MC_LIST_ID}
     restart: unless-stopped
     networks:
       - backend
diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts
index 97ba73ce36e4fb5f24927bfea36ac140d918162d..675158f30b5261dc42d2ace11d146b28c37b5981 100644
--- a/src/admin/admin.controller.spec.ts
+++ b/src/admin/admin.controller.spec.ts
@@ -328,26 +328,6 @@ describe('AdminController', () => {
     });
   });
 
-  describe('Search user newsletter subscription', () => {
-    it('should return all subscribed users, empty string', async () => {
-      expect((await controller.getNewsletterSubscriptions({ searchString: '' })).length).toBe(3);
-    });
-    it('should return all subscribed users, null input', async () => {
-      expect((await controller.getNewsletterSubscriptions({ searchString: null })).length).toBe(3);
-    });
-    it('should find one user', async () => {
-      expect((await controller.getNewsletterSubscriptions({ searchString: 'a@a.com' })).length).toBe(1);
-      expect((await controller.getNewsletterSubscriptions({ searchString: 'a@a.com' }))[0].email).toBe('a@a.com');
-    });
-    it('should find no user', async () => {
-      expect((await controller.getNewsletterSubscriptions({ searchString: 'adgdgsdg@a.com' })).length).toBe(0);
-    });
-  });
-
-  it('should count user subscribed to newsletter', async () => {
-    expect(await controller.countNewsletterSubscriptions()).toBe(246);
-  });
-
   describe('Search delete a user subscription', () => {
     it('should return a deleted object', async () => {
       expect((await controller.unsubscribeUserFromNewsletter('a@a.com')).email).toBe('a@a.com');
diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts
index 0a9bf0cd9e6ea35381780f2e67de77cc8f77b256..415bad28c53aa1b8f35abd9301e1a5ea692041f0 100644
--- a/src/admin/admin.controller.ts
+++ b/src/admin/admin.controller.ts
@@ -251,24 +251,6 @@ export class AdminController {
     });
   }
 
-  @UseGuards(JwtAuthGuard, RolesGuard)
-  @Roles('admin')
-  @ApiBearerAuth('JWT')
-  @Post('searchNewsletterSubscriptions')
-  public async getNewsletterSubscriptions(@Body() searchString: { searchString: string }) {
-    if (searchString && searchString.searchString && searchString.searchString.length > 0)
-      return this.newsletterService.searchNewsletterSubscription(searchString.searchString);
-    else return this.newsletterService.findAll();
-  }
-
-  @UseGuards(JwtAuthGuard, RolesGuard)
-  @Roles('admin')
-  @ApiBearerAuth('JWT')
-  @Get('countNewsletterSubscriptions')
-  public async countNewsletterSubscriptions(): Promise<number> {
-    return this.newsletterService.countNewsletterSubscriptions();
-  }
-
   @UseGuards(JwtAuthGuard, RolesGuard)
   @Roles('admin')
   @ApiBearerAuth('JWT')
diff --git a/src/newsletter/newsletter.service.spec.ts b/src/newsletter/newsletter.service.spec.ts
index c79529572d1af9aeadf4533f1c9166876d25f457..60c91fd90203b6e8ecb7172ef76e38c8dc179a7b 100644
--- a/src/newsletter/newsletter.service.spec.ts
+++ b/src/newsletter/newsletter.service.spec.ts
@@ -51,24 +51,21 @@ describe('NewsletterService', () => {
   });
 
   describe('newsletterSubscribe', () => {
-    it('it should not add subscription for email test2@test.com : already exist', async () => {
-      const result = { email: 'test2@test.com' } as INewsletterSubscription;
+    it('it should add subscription for email test2@test.com even if it exists', async () => {
+      const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription;
+      mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
       jest
         .spyOn(service, 'findOne')
-        .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result);
-      try {
-        await service.newsletterSubscribe('test2@test.com');
-        // Fail test if above expression doesn't throw anything.
-        expect(true).toBe(false);
-      } catch (e) {
-        expect(e.message).toEqual('Email already exists');
-        expect(e.status).toEqual(HttpStatus.BAD_REQUEST);
-      }
+        .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => _doc);
+      mockNewsletterModel.create.mockResolvedValueOnce(_doc);
+
+      const subscription = await service.newsletterSubscribe('test2@test.com');
+      expect(subscription).toEqual(_doc);
     });
     it('it should add a subscription for email test2@test.com', async () => {
-      const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription;
+      const result = { email: 'test2@test.com' } as INewsletterSubscription;
       const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
-      mailchimp.lists.addListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
+      mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
       jest
         .spyOn(service, 'findOne')
         .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
@@ -76,12 +73,12 @@ describe('NewsletterService', () => {
       mockNewsletterModel.create.mockResolvedValueOnce(_doc);
 
       const subscription = await service.newsletterSubscribe('test2@test.com');
-      expect(subscription).toEqual({ email: 'test2@test.com' });
+      expect(subscription).toEqual(_doc);
     });
-    it('it should return mailchimp 400 issue', async () => {
-      const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription;
+    it('it should return error if mailchimp 400 issue', async () => {
+      const result = { email: 'test2@test.com' } as INewsletterSubscription;
       const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
-      mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 400 });
+      mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 400 });
       jest
         .spyOn(service, 'findOne')
         .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
@@ -92,14 +89,14 @@ describe('NewsletterService', () => {
         await service.newsletterSubscribe('test2@test.com');
         expect(true).toBe(false);
       } catch (e) {
-        expect(e.message).toBe('Email already exists');
-        expect(e.status).toEqual(HttpStatus.BAD_REQUEST);
+        expect(e.message).toBe('Subscribe error');
+        expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
       }
     });
-    it('it should return mailchimp 500 issue', async () => {
-      const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription;
+    it('it should return error if mailchimp 500 issue', async () => {
+      const result = { email: 'test2@test.com' } as INewsletterSubscription;
       const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
-      mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 500 });
+      mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 500 });
       jest
         .spyOn(service, 'findOne')
         .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined)
@@ -110,28 +107,27 @@ describe('NewsletterService', () => {
         await service.newsletterSubscribe('test2@test.com');
         expect(true).toBe(false);
       } catch (e) {
-        expect(e.message).toBe('Server error');
+        expect(e.message).toBe('Subscribe error');
         expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
       }
     });
   });
   describe('newsletterUnsubscribe', () => {
     it('it should not remove subscription for email test@test.com : does not exist', async () => {
-      const result: INewsletterSubscription = undefined;
-      jest
-        .spyOn(service, 'findOne')
-        .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result);
+      mailchimp.lists.getListMember.mockRejectedValueOnce({ status: 404 });
       try {
         await service.newsletterUnsubscribe('test@test.com');
         // Fail test if above expression doesn't throw anything.
         expect(true).toBe(false);
       } catch (e) {
-        expect(e.message).toEqual('Invalid email');
-        expect(e.status).toEqual(HttpStatus.BAD_REQUEST);
+        expect(e.message).toEqual('Email not found');
+        expect(e.status).toEqual(HttpStatus.NOT_FOUND);
       }
     });
     it('it should remove a subscription for email test2@test.com', async () => {
       const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' };
+      mailchimp.lists.getListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
+      mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' });
       const result = {
         email: 'test2@test.com',
         deleteOne: async () => _doc,
@@ -145,15 +141,6 @@ describe('NewsletterService', () => {
     });
   });
 
-  describe('countNewsletterSubscriptions', () => {
-    it('it should count subscriptions', async () => {
-      mockNewsletterModel.countDocuments.mockResolvedValueOnce(69);
-      const count = await service.countNewsletterSubscriptions();
-
-      expect(count).toEqual(69);
-    });
-  });
-
   describe('findOne', () => {
     it('it should not find a subscription with email test@test.com', async () => {
       mockNewsletterModel.findOne.mockResolvedValueOnce(undefined);
@@ -175,18 +162,6 @@ describe('NewsletterService', () => {
       expect(findOneEmail).toEqual(_docs);
     });
   });
-  describe('searchNewsletterSubscription', () => {
-    it('it should find 2 search result', async () => {
-      const _docs = [
-        { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription,
-        { _id: 'bbbbb', email: 'test@test.com' } as INewsletterSubscription,
-      ];
-      mockNewsletterModel.find.mockResolvedValueOnce(_docs);
-      const findOneEmail = await service.searchNewsletterSubscription('test');
-      expect(findOneEmail.length).toBe(2);
-    });
-  });
-
   describe('updateNewsletterSubscription', () => {
     it('should update existing user subscription', () => {
       mailchimp.lists.getListMembersInfo.mockResolvedValueOnce({ total_items: 10 }).mockResolvedValueOnce({
@@ -198,10 +173,9 @@ describe('NewsletterService', () => {
       });
       const result = { email: 'test2@test.com' } as INewsletterSubscription;
       const spyer = jest.spyOn(mockNewsletterModel, 'findOne');
-      // jest.spyOn(service, 'findOne').mockResolvedValueOnce(result).mockResolvedValueOnce(result);
       mockNewsletterModel.findOne.mockResolvedValueOnce(result).mockResolvedValueOnce(null);
       service.updateNewsletterSubscription();
-      expect(spyer).toBeCalledTimes(2);
+      expect(spyer).toBeCalledTimes(3);
       // expect(spyerDelete).toBeCalledTimes(1);
     });
   });
diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts
index 7efffd693731a043e1b0566dde4f9c2e3fc25b48..984de7a3926703550808b158ed2b84ef27d37914 100644
--- a/src/newsletter/newsletter.service.ts
+++ b/src/newsletter/newsletter.service.ts
@@ -2,11 +2,12 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
 import { InjectModel } from '@nestjs/mongoose';
 import { Cron, CronExpression } from '@nestjs/schedule';
 import { Model } from 'mongoose';
+import { md5 } from '../shared/utils';
 import { IMailchimpSubscription } from './interface/mailchimp-subscription';
 import { INewsletterSubscription } from './interface/newsletter-subscription.interface';
-import { NewsletterSubscription, NewsletterSubscriptionDocument } from './newsletter-subscription.schema';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const mailchimp = require('@mailchimp/mailchimp_marketing');
+import { NewsletterSubscription } from './newsletter-subscription.schema';
+import mailchimp = require('@mailchimp/mailchimp_marketing');
+
 @Injectable()
 export class NewsletterService {
   private readonly logger = new Logger(NewsletterService.name);
@@ -41,40 +42,60 @@ export class NewsletterService {
   }
 
   public async newsletterSubscribe(email: string): Promise<NewsletterSubscription> {
-    this.logger.debug('newsletterSubscribe');
-    const existingEmail = await this.findOne(email);
-    if (existingEmail) {
-      throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
-    }
+    this.logger.debug(`newsletterSubscribe: ${email}`);
+    email = email.toLocaleLowerCase();
     try {
-      const member = await mailchimp.lists.addListMember(this.LIST_ID, {
+      // Add or update list member (to be able to subscribe again a member who had already subscribed then unsubscribed)
+      // (the second parameter is the MD5 hash of the lowercase email, we actually don't need to maintain a mapping in newsletterSubscription : cf. https://mailchimp.com/developer/marketing/docs/methods-parameters/#path-parameters )
+      const member = await mailchimp.lists.setListMember(this.LIST_ID, md5(email), {
         email_address: email,
+        status_if_new: 'subscribed', // cf. https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/
         status: 'subscribed',
       });
-      await this.newsletterSubscriptionModel.create({ email: email, mailchimpId: member.id });
-      return this.findOne(email);
-    } catch (e) {
-      if (e.status === 400) {
-        this.logger.error(`Error ${e.status}, user might already exist in mailchimplist`);
-        throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
-      } else {
-        this.logger.error(`Mailchimp configuration error`);
-        throw new HttpException('Server error', HttpStatus.INTERNAL_SERVER_ERROR);
+
+      // We may not be aware that the user has unsubscribed from the newsletter, so it is ok if it already exists in newsletterSubscription
+      let newsletterSubscription = await this.findOne(email);
+      if (!newsletterSubscription) {
+        newsletterSubscription = await this.newsletterSubscriptionModel.create({
+          email: email,
+          mailchimpId: member.id,
+        });
       }
+      return newsletterSubscription;
+    } catch (e) {
+      this.logger.error(`newsletterSubscribe ${email}: ${JSON.stringify(e)}`);
+      throw new HttpException('Subscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
     }
   }
 
   public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> {
-    this.logger.debug('newsletterUnsubscribe');
-    const subscription = await this.findOne(email);
-    if (!subscription) {
-      throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST);
+    this.logger.debug(`newsletterUnsubscribe: ${email}`);
+    email = email.toLocaleLowerCase();
+    const emailMd5Hashed = md5(email);
+
+    let newsletterSubscription = await this.findOne(email);
+    if (newsletterSubscription) {
+      newsletterSubscription = newsletterSubscription.deleteOne();
     }
-    await mailchimp.lists.setListMember(this.LIST_ID, subscription.mailchimpId, {
-      email_address: email,
-      status: 'unsubscribed',
-    });
-    return subscription.deleteOne();
+
+    try {
+      const response = await mailchimp.lists.getListMember(this.LIST_ID, emailMd5Hashed);
+      if (response.status === 'unsubscribed') {
+        throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
+      }
+      await mailchimp.lists.setListMember(this.LIST_ID, emailMd5Hashed, {
+        status: 'unsubscribed',
+      });
+    } catch (e) {
+      if (e.status === 404) {
+        throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
+      } else {
+        this.logger.error(`newsletterUnsubscribe ${email}: ${JSON.stringify(e)}`);
+        throw new HttpException('Unsubscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
+      }
+    }
+
+    return newsletterSubscription;
   }
 
   public async findOne(mail: string): Promise<INewsletterSubscription | undefined> {
@@ -82,16 +103,6 @@ export class NewsletterService {
     return this.newsletterSubscriptionModel.findOne({ email: mail });
   }
 
-  public async searchNewsletterSubscription(searchString: string): Promise<NewsletterSubscriptionDocument[]> {
-    this.logger.debug('searchNewsletterSubscription');
-    return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') });
-  }
-
-  public async countNewsletterSubscriptions(): Promise<number> {
-    this.logger.debug('countNewsletterSubscriptions');
-    return this.newsletterSubscriptionModel.countDocuments({});
-  }
-
   public async findAll(): Promise<NewsletterSubscription[]> {
     this.logger.debug('findAll');
     return this.newsletterSubscriptionModel.find();
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 0283a1a3a31d32c1b4dca17fa8abce7b7d5ac955..625df75b54351cdb931f234bcd6ed1c385147f24 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -4,6 +4,9 @@ import { Page } from '../pages/schemas/page.schema';
 import { Post } from '../posts/schemas/post.schema';
 import { UserRole } from '../users/enum/user-role.enum';
 import { User } from '../users/schemas/user.schema';
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const crypto = require('crypto');
+export const md5 = (data: string): string => crypto.createHash('md5').update(data).digest('hex');
 
 export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post {
   // Handle image display. Rewrite image URL to fit ghost infra issue.